{"id":22898595,"url":"https://github.com/jclem/router","last_synced_at":"2026-02-12T17:03:12.284Z","repository":{"id":212333863,"uuid":"729276668","full_name":"jclem/router","owner":"jclem","description":"A minimal router for building Bun HTTP services","archived":false,"fork":false,"pushed_at":"2025-02-05T21:36:13.000Z","size":47,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-13T12:48:12.133Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://npm.im/@jclem/router","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jclem.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-12-08T19:36:43.000Z","updated_at":"2024-12-04T21:02:38.000Z","dependencies_parsed_at":"2023-12-18T18:54:04.769Z","dependency_job_id":"fa0ee7c7-0b30-40d1-a519-40476c51a8ad","html_url":"https://github.com/jclem/router","commit_stats":null,"previous_names":["jclem/toad","jclem/router"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/jclem/router","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jclem%2Frouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jclem%2Frouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jclem%2Frouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jclem%2Frouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jclem","download_url":"https://codeload.github.com/jclem/router/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jclem%2Frouter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29373837,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-12T08:51:36.827Z","status":"ssl_error","status_checked_at":"2026-02-12T08:51:26.849Z","response_time":55,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-14T00:34:09.157Z","updated_at":"2026-02-12T17:03:12.265Z","avatar_url":"https://github.com/jclem.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Router\n\nRouter is a minimal router for building Bun HTTP services.\n\n## Installation\n\n```shell\nbun add @jclem/router\n```\n\n## Use\n\n### Routing\n\nRouting in Router is a matter of assigning handlers to HTTP methods and paths.\nHere is an example of a simple Router router:\n\n```ts\nimport { createRouter } from \"@jclem/router\";\nimport { expect } from \"bun:test\";\n\nconst router = createRouter()\n  .get(\"/\", () =\u003e Response.json({ ok: true }))\n  .get(\"/:foo\", ({ parameters }) =\u003e Response.json(parameters))\n  .get(\"/:foo/bar/*\", ({ parameters }) =\u003e Response.json(parameters));\n\nlet response = await router.handle(new Request(\"http://example.com\"));\nexpect(await response.json\u003cunknown\u003e()).toEqual({ ok: true });\n\nresponse = await router.handle(new Request(\"http://example.com/foo\"));\nexpect(await response.json\u003cunknown\u003e()).toEqual({ foo: \"foo\" });\n\nresponse = await router.handle(\n  new Request(\"http://example.com/foo/bar/baz/qux\"),\n);\nexpect(await response.json\u003cunknown\u003e()).toEqual({ foo: \"foo\", \"*\": \"baz/qux\" });\n```\n\n#### Sub-routers\n\nRouter supports sub-routers, where a new router is mounted at a given path. This router\nwill inherit the middleware stack of the parent router.\n\n```ts\nimport { createRouter, createMiddleware } from \"@jclem/router\";\nimport { expect } from \"bun:test\";\n\nconst router = createRouter()\n  .use(createMiddleware(() =\u003e ({ a: 1 })))\n  .route(\"/foo\", (t) =\u003e\n    t\n      .use(createMiddleware(() =\u003e ({ b: 2 })))\n      .get(\"/\", (ctx) =\u003e Response.json(ctx.locals))\n      .route(\"/bar\", (t) =\u003e\n        t\n          .use(createMiddleware(() =\u003e ({ c: 3 })))\n          .get(\"/\", (ctx) =\u003e Response.json(ctx.locals)),\n      ),\n  )\n  .get(\"/\", (ctx) =\u003e Response.json(ctx.locals));\n\nlet resp = await router.handle(new Request(\"http://example.com\"));\nexpect(resp.status).toBe(200);\nexpect(await resp.json\u003cunknown\u003e()).toEqual({ a: 1 });\n\nresp = await router.handle(new Request(\"http://example.com/foo\"));\nexpect(resp.status).toBe(200);\nexpect(await resp.json\u003cunknown\u003e()).toEqual({ a: 1, b: 2 });\n\nresp = await router.handle(new Request(\"http://example.com/foo/bar\"));\nexpect(resp.status).toBe(200);\nexpect(await resp.json\u003cunknown\u003e()).toEqual({ a: 1, b: 2, c: 3 });\n```\n\nNote that Router isn't an HTTP _server_, it's just a router. In order to invoke\nthe router, just pass it a `Request` via its `handle(request: Request)` method,\nlike the one you get from a Bun HTTP server handler. This `handle` method\nreturns a `Response` or a Promise resolving to a `Response`.\n\n### Middleware\n\nRouter uses one method for attaching middleware, called `use`. Middleware is\nalways invoked before route matching happens, so even when no route is matched,\nmiddleware is still invoked.\n\nThe easiest way to write middleware is to use the `createMiddleware` function\nprovided by Router. The return value (if one is present) of the function given to\n`createMiddleware` will be merged into the request \"locals\", which will be\navailable to middleware further down the stack.\n\nAs seen below in `logRequest`, `createMiddleware` can also accept a second\nfunction, which will run after the handler is called. It receives the context\nargument as well as the response.\n\n```ts\nimport { createRouter, createMiddleware } from \"@jclem/router\";\nimport crypto from \"node:crypto\";\n\nconst assignRequestID = createMiddleware(({ request }) =\u003e {\n  const requestID = request.headers.get(\"Request-ID\") || crypto.randomUUID();\n  return { requestID };\n});\n\nconst logRequest = createMiddleware(\n  ({ request, locals }) =\u003e {\n    return { startTime: process.hrtime.bigint() };\n  },\n  ({ request, locals }, response) =\u003e {\n    const elapsed = process.hrtime.bigint() - locals.startTime;\n\n    console.log(\n      `${request.method} ${request.url} ${response.status} ${elapsed}ns`,\n    );\n  },\n);\n\nconst router = createRouter()\n  .use(assignRequestID)\n  .use(logRequest)\n  .get(\"/\", () =\u003e Response.json({ ok: true }));\n```\n\nNote that `createMiddleware` is just a convenience function. You can also\nmanually write middleware, should you choose to do so, but it is a little more\ndifficult to deal with types (they'll be under-specified, but not inaccurate).\nWe could write the above module without `createMiddleware`. In order to do so: A\nmiddleware takes two arguments: The incoming `BeforeCtx\u003cLocals\u003e` object, and a\n`Next\u003cNewLocals\u003e` callback function, which returns a `Response`.\n\nSo, the basic raw middlware flow looks like this:\n\n1. Do something with the incoming context before the request.\n2. Call `next`, with the new locals. The calls the remaining middleware and the\n   request handler.\n3. Do something with the response and the new locals.\n4. Return the response.\n\nNote that due to how middlewares are stacked, only the first \"half\" of all\nremaining middleware will run when `next()` in called. This allows you to\neffectively have \"before\" and \"after\" middleware using the same function.\n\nIt looks like this:\n\n```ts\nimport { BeforeCtx, Next, createRouter } from \"@jclem/router\";\nimport crypto from \"node:crypto\";\n\nfunction assignRequestID(\n  { request, locals }: BeforeCtx\u003c{}, {}\u003e,\n  next: Next\u003c{ requestID: string }\u003e,\n) {\n  const requestID = request.headers.get(\"Request-ID\") || crypto.randomUUID();\n  return next({ ...locals, requestID });\n}\n\nasync function logRequest(\n  { request, locals }: BeforeCtx\u003c{ requestID: string }, {}\u003e,\n  next: Next\u003c{ requestID: string }\u003e,\n) {\n  const startTime = process.hrtime.bigint();\n  const response = await next(locals);\n  const elapsedMs = Number(process.hrtime.bigint() - startTime) / 1e6;\n  console.log(\n    `${request.method} ${request.url} ${response.status} ${elapsedMs}ms`,\n  );\n  return response;\n}\n\nconst router = createRouter()\n  .use(assignRequestID)\n  .use(logRequest)\n  .get(\"/\", () =\u003e Response.json({ ok: true }));\n```\n\nTo write this in a more type-safe manner, use the exported types such as\n`Next\u003cO\u003e` and `Middleware\u003cI, O\u003e` provided by Router:\n\n```ts\nimport { Middleware, createRouter } from \"@jclem/router\";\nimport crypto from \"node:crypto\";\n\nfunction assignRequestID\u003cI, P\u003e(): Middleware\u003cI, I \u0026 { requestID: string }, P\u003e {\n  return function ({ request, locals }, next) {\n    const requestID = request.headers.get(\"request-id\") || crypto.randomUUID();\n    return next({ ...locals, requestID });\n  };\n}\n\nfunction logRequest\u003cI extends { requestID: string }, P\u003e(): Middleware\u003cI, I, P\u003e {\n  return async function ({ request, locals }, next) {\n    const startTime = process.hrtime.bigint();\n    const response = await next(locals);\n    const elapsedMs = Number(process.hrtime.bigint() - startTime) / 1e6;\n    console.log(\n      `${locals.requestID} ${request.method} ${request.url} ${response.status} ${elapsedMs}ms`,\n    );\n    return response;\n  };\n}\n\nconst router = createRouter()\n  .use(assignRequestID())\n  .use(logRequest())\n  .get(\"/\", () =\u003e Response.json({ ok: true }));\n```\n\n### Serving Real Requests\n\nIn order to serve real requests, just call `router.handle` in a Bun HTTP server\nrequest handler.\n\n```ts\nimport { createRouter } from \"router\";\n\nconst router = createRouter().get(\"/\", () =\u003e Response.json({ ok: true }));\n\nBun.serve({\n  port: 3000,\n  fetch(request) {\n    return router.handle(request);\n  },\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjclem%2Frouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjclem%2Frouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjclem%2Frouter/lists"}