npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dynara

v0.0.6

Published

Simple HTTP framework powered by Bun

Readme

Dynara

NPM version

An extremely simple HTTP framework for Bun — practically a typed wrapper around Bun.serve, with Fastify-style routing and fast schema validation. Made for people switching over from Fastify.

  • Bun only — Node.js and Deno are not supported.
  • Minimal overhead — routing is delegated to Bun's native router.
  • Typed validation — powered by TypeBox, written with the compact compact-json-schema syntax.

Install

bun add dynara

Quick start

import { Router } from 'dynara'

const app = new Router()

app.get('/', () => {
  return { hello: 'world' }
})

app.listen(3000)

A handler may return a plain value (sent as JSON), a Response (sent as-is), or undefined (an empty 200).

Routes & validation

Routes use Bun's native patterns — :param for a single segment, * for a wildcard. The methods are get, post, put, patch, and delete.

Schemas are written with compact-json-schema and validated with TypeBox. Pass them as a route-option object, or as a positional array — [params] / [params, query] for GET, [params, body, query] for the others. Validated req.params, req.query, and req.body are fully typed.

import { schema } from 'compact-json-schema'

const params = schema({ id: 'number' })
const body = schema({ name: 'string', age: 'number?' }) // ? optional, ?? nullable

// Option object
app.post('/users/:id', { params, body }, (req) => {
  req.params.id   // number
  req.body.name   // string
  return { ok: true }
})

// Positional array
app.get('/users/:id', [params], (req) => {
  return { id: req.params.id } // number
})

A few conveniences:

  • Array params are comma-split: GET /items/1,2,3 with { itemIds: { type: 'array', items: 'number' } } yields [1, 2, 3].
  • Query booleans accept ?flag=true or a bare ?flag.
  • Dates: a "date" field accepts an ISO-8601 string ("2020-01-02", optionally with a time part) or epoch milliseconds, and is decoded into a JS Date before your handler runs — req.body.startsAt instanceof Date. Works in bodies, query strings, and params; "date?" / "date??" behave like every other type.
  • req.raw exposes the underlying BunRequest, and req.server the Bun Server.
const event = schema({ title: 'string', startsAt: 'date', endsAt: 'date??' })
app.post('/events', { body: event }, (req) => {
  return { day: req.body.startsAt.getDay() } // startsAt is a Date
})

Hooks

app.addHook('onRequest', (req) => {
  // runs before every handler; throw to short-circuit the request
})

app.addHook('onResponse', (req, res) => {
  // runs after the response is produced (incl. errors and 404s); observational
  console.log(`${req.raw.method} ${new URL(req.raw.url).pathname} -> ${res.status}`)
})

app.addHook('onListen', (server) => {
  console.log(`Listening on ${server.url}`)
})

onRequest runs before body parsing/validation; throwing short-circuits into the error handler. onResponse runs after a Response exists — from a handler, an error, or a 404 — and can read res.status / res.headers. It is observational: its return value is ignored, and a throw is logged but never changes the response.

Hooks propagate into register() children. A hook added on the root (or any ancestor) runs for every route mounted under it, outermost→innermost, before the child's own hooks — so a root-level onRequest for auth/request-id/logging applies everywhere. Encapsulation stays one-directional: a hook added inside a child never affects the parent or sibling routers.

app.addHook('onRequest', (req) => { req.requestId = crypto.randomUUID() })
app.register((api) => {
  api.get('/health', (req) => ({ id: req.requestId })) // sees the root hook
}, { prefix: '/api' })

Latency timing

There is no built-in timing API — stamp a start time in onRequest and read it back in onResponse. Type the app's context so the field is available on req:

const app = new Router<{ startedAt?: number }>()
app.addHook('onRequest', (req) => { req.startedAt = performance.now() })
app.addHook('onResponse', (req, res) => {
  console.log(`${res.status} in ${performance.now() - (req.startedAt ?? 0)}ms`)
})

Plugins & context

register mounts a group of routes under a prefix. Type the app with a context type to share data attached by hooks:

type Ctx = { user: { id: number } }

app.register((users: Router<Ctx>) => {
  users.addHook('onRequest', (req) => { req.user = { id: 1 } })
  users.get('/me', (req) => req.user)
}, { prefix: '/users' })

For composable, reusable plugins there is the dynara() builder. use adds plugins, routes adds handlers, and the result can be passed to register:

import { dynara, Router } from 'dynara'

const useAuth = (app: Router<Ctx>) => {
  app.addHook('onRequest', (req) => { req.user = { id: 1 } })
}

const users = dynara<Ctx>()
  .use(useAuth)
  .routes((app) => {
    app.get('/me', (req) => req.user)
  })

app.register(users, { prefix: '/users' })

Per-route guards with .with()

To guard a single route without wrapping it in a whole sub-group, use app.with(plugin). It takes the same plugin shape as .use(), so the same guard works at both group level (.use()) and route level (.with()). It is type-contributing — whatever the plugin guarantees on req (e.g. req.auth) is typed as present in the handler — and one-shot: the guard applies only to the single route method called on the scope. Chain .with(a).with(b) to compose.

const useAuth = (app: Router<{ auth: { userId: number } }>) => {
  app.addHook('onRequest', (req) => {
    const token = req.raw.headers.get('authorization')
    if (!token) throw new HTTPError('Unauthorized', 401)
    req.auth = { userId: Number(token) }
  })
}

export default dynara().routes((app) => {
  app.post('/request-code', { body }, handler)                    // public
  app.post('/verify', { body }, handler)                          // public
  app.with(useAuth).post('/complete-profile', { body }, handler)  // guarded — req.auth guaranteed
  app.with(useAuth).get('/me', (req) => req.auth)                 // guarded
})

Guards run in the onRequest phase — after group-level / propagated parent hooks and before body validation.

Errors

Throw HTTPError to send an explicit status code; validation failures are turned into 400 responses automatically.

import { HTTPError } from 'dynara'

app.get('/secret', () => {
  throw new HTTPError('Forbidden', 403)        // text body
  // throw new HTTPError({ reason: 'forbidden' }, 403)  // JSON body
})

Custom error handler

Install an app-global handler with setErrorHandler to control the error envelope, hide internals in production, or forward errors to Sentry. It receives the raw error (so it can special-case HTTPError / validation errors) plus the request, and returns the Response. Every error from a hook or handler flows through it, including register() children. If the handler itself throws, dynara falls back to the built-in mapping and never crashes.

import { HTTPError, isValidationError } from 'dynara'

app.setErrorHandler((err, req) => {
  if (err instanceof HTTPError) {
    return Response.json({ error: err.message }, { status: err.statusCode })
  }
  if (isValidationError(err)) {
    return Response.json({ error: 'Invalid input' }, { status: 400 })
  }
  Sentry.captureException(err)
  return Response.json({ error: 'Internal error' }, { status: 500 })
})

Testing

inject() dispatches a request through your routes in-process — no server, no socket — and returns a real Response. It reuses the same routing, validation, and error handling as a live server.

import { test, expect } from 'bun:test'

test('returns a user', async () => {
  const res = await app.inject('/users/1')
  expect(res.status).toBe(200)
  expect(await res.json()).toEqual({ id: 1 })
})

// With a body:
await app.inject({ method: 'POST', url: '/users', body: { name: 'Alice' } })

Under inject() there is no Bun Server, so req.server is undefined and WebSocket upgrades are not exercised.

WebSockets

app.registerWsHandler('/ws', {
  open(ws) { ws.send('hello') },
  message(ws, msg) { ws.send(msg) },
})

Changelog

Unreleased

  • Breaking (pre-1.0): onRequest hooks now propagate into register() children. Previously a hook added on the root did not run for routes mounted under a prefixed sub-router. Now ancestor hooks run for descendant routes (outermost→innermost), matching Fastify. Hooks added inside a child remain scoped to that child and its descendants — they never affect a parent or sibling. If you relied on root hooks not applying to a sub-router, move that hook into the specific child instead.
  • New: Router.setErrorHandler(handler) for a custom, app-global error response, plus the isValidationError(err) type guard.
  • New: onResponse lifecycle hook, fired for every produced response (including errors and 404s).
  • New: Router.with(plugin) for a type-contributing per-route guard that reuses the .use() plugin shape.

License

MIT