{"id":17057023,"url":"https://github.com/yuhr/routets","last_synced_at":"2026-04-29T08:05:27.946Z","repository":{"id":176482826,"uuid":"657929640","full_name":"yuhr/routets","owner":"yuhr","description":"Vanilla filesystem-based routing for TypeScript.","archived":false,"fork":false,"pushed_at":"2025-08-07T02:23:27.000Z","size":246,"stargazers_count":7,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2026-04-22T16:15:03.211Z","etag":null,"topics":["deno","http-router","http-routing","http-server","typescript","web-framework"],"latest_commit_sha":null,"homepage":"https://routets-examples.deno.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yuhr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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-06-24T08:40:11.000Z","updated_at":"2025-10-06T14:10:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"c2ae8c4f-3a94-4b20-934a-0fd141689125","html_url":"https://github.com/yuhr/routets","commit_stats":null,"previous_names":["yuhr/routets"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/yuhr/routets","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuhr%2Froutets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuhr%2Froutets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuhr%2Froutets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuhr%2Froutets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yuhr","download_url":"https://codeload.github.com/yuhr/routets/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuhr%2Froutets/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32416149,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T06:29:02.080Z","status":"ssl_error","status_checked_at":"2026-04-29T06:29:00.631Z","response_time":110,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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":["deno","http-router","http-routing","http-server","typescript","web-framework"],"created_at":"2024-10-14T10:26:15.205Z","updated_at":"2026-04-29T08:05:27.941Z","avatar_url":"https://github.com/yuhr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\u003cbr\u003e\u003cbr\u003e\n\n# ROUTETS\n\n[![License](https://img.shields.io/github/license/yuhr/routets?color=%231e2327)](LICENSE)\n\nVanilla filesystem-based routing for TypeScript.\n\n\u003cbr\u003e\u003cbr\u003e\u003c/div\u003e\n\n`routets` is an HTTP request handler generator that performs filesystem-based routing.\n\nNo other stuff. That's all. I was always tired of fullstack frameworks such as Fresh or Aleph.js, because of the tightly coupled design that forces users to be on the rails. So I ended up making this stupid-simple solution, which is aimed to be:\n\n- No magic and no blackbox\n- Small size and less dependency\n- No squatted pathnames like `/index`, `/_app`, or `/_404`\n- No lock-in to a specific JSX implementation\n- No lock-in to a specific architecture; MPA or SPA, SSR or CSR, etc.\n- Use of Web standard APIs\n\nSo, `routets` is deliberately less-featured. It just provides a basic building block for writing web servers in TypeScript, leveraging Create Your Own™ style of experience.\n\n`routets` primarily targets Deno and Deno Deploy, but its core APIs (i.e. `Router` and `Route`) are meant to work across multiple runtimes.\n\n## Basic Usage\n\nCreate a file with the filename being `\u003cyour-route-name\u003e.route.ts`, say `./greet.route.ts` here and the content is like this:\n\n```ts\nimport Route from \"https://deno.land/x/routets/Route.ts\"\n\nexport default new Route(async () =\u003e {\n\treturn new Response(\"Hello, World!\")\n})\n```\n\n`routets` comes with a built-in CLI for Deno. During development, you can use this and serve your routes immediately:\n\n```sh\n$ deno install -gAf https://deno.land/x/routets/routets.ts\n$ routets # or `routets somewhere` to serve `somewhere/greet.route.ts` at `/greet`\nListening on http://0.0.0.0:8000/ (http://localhost:8000/)\nRoutes:\n+ /greet\n```\n\nAnd you'll see “Hello, World!” at [`http://localhost:8000/greet`](http://localhost:8000/greet).\n\nAlternatively, of course you can create your own script:\n\n```ts\nimport Router from \"https://deno.land/x/routets/Router.ts\"\n\nawait Deno.serve(new Router({ root: \".\", watch: true })).finished\n```\n\n## Advanced Usage\n\n### Dynamic Routes\n\n`routets` supports dynamic routes by [URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API). Please refer to the MDN documentation for the syntax and examples.\n\nCaptured parts of the pathname will be available in the first parameter of the handler. For example, when you have `:dynamic.route.ts` with the content being:\n\n```ts\nimport Route from \"https://deno.land/x/routets/Route.ts\"\n\nexport default new Route(async ({ captured }) =\u003e {\n\treturn new Response(JSON.stringify(captured), { headers: { \"Content-Type\": \"application/json\" } })\n})\n```\n\nAccessing `/route` will show you `{\"dynamic\":\"route\"}`.\n\n### Route Precedence\n\nOnce you have started using dynamic routes, you may notice it is unclear which route will be matched when multiple routes are valid for the requested pathname. For example, if you have files named `.route.ts` and `*.route.ts`, which one will be matched when you access `/`?\n\nBy default, `routets` doesn't do anything smart, and just performs **codepoint-wise reverse-lexicographic ordering** of **pathname patterns** (not of actual file paths, which include the suffix and the extension). So, in the above example, `*.route.ts` will win, as `/*` precedes `/` reverse-lexicographically. If you want to change this behavior, just named-export a number as `precedence` from each route:\n\n```ts\n// in `.route.ts`\nexport const precedence = 1\n```\n\nIf `precedence` is not exported, it implies `0`.\n\nRoutes with greater precedences win. Think of it like `z-index` in CSS. So, at this time `.route.ts` will be matched first. You can always confirm the ordering by seeing the output of `routets` (routes listed earlier win):\n\n```sh\n$ routets\nListening on http://0.0.0.0:8000/\nRoutes:\n+ /\n+ /*\n```\n\n### Route Fallthrough\n\nIf a route returns nothing (namely `undefined`), then it fallthroughs to the next matching route.\n\n### Extending `Route`\n\nIf you want to insert middlewares before/after an execution of handlers, you can extend the `Route` class as usual in TypeScript.\n\nTo exercise this, here we add support for returning a React element from handlers!\n\n```tsx\nimport Route from \"https://deno.land/x/routets/Route.ts\"\nimport { renderToReadableStream } from \"https://esm.sh/react-dom@19.1.0/server\"\nimport { type ReactElement, Suspense } from \"https://esm.sh/react@19.1.0\"\n\nclass RouteReact extends Route {\n\tconstructor(handler: Route.Handler\u003cReactElement\u003cunknown\u003e\u003e) {\n\t\tsuper(async context =\u003e {\n\t\t\tconst response = await handler(context)\n\t\t\treturn new Response(\n\t\t\t\tawait renderToReadableStream(\n\t\t\t\t\t\u003chtml\u003e\n\t\t\t\t\t\t\u003cbody\u003e\n\t\t\t\t\t\t\t\u003cSuspense fallback={\u003cp\u003eLoading...\u003c/p\u003e}\u003e{response}\u003c/Suspense\u003e\n\t\t\t\t\t\t\u003c/body\u003e\n\t\t\t\t\t\u003c/html\u003e,\n\t\t\t\t),\n\t\t\t\t{ headers: { \"Content-Type\": \"text/html\" } },\n\t\t\t)\n\t\t})\n\t}\n}\n\nexport default RouteReact\n```\n\nAnd don't forget to add following options to your `deno.json`:\n\n```jsonc\n{\n\t\"compilerOptions\": {\n\t\t\"jsx\": \"react-jsx\",\n\t\t\"jsxImportSource\": \"https://esm.sh/react@19.1.0\"\n\t}\n}\n```\n\nThat's it! You can now create a route using it, e.g. with the filename being `.route.tsx`:\n\n```tsx\nimport RouteReact from \"./RouteReact.ts\"\nimport { delay } from \"https://esm.sh/jsr/@std/async@1.0.12/delay.ts\"\n\nlet done = false\nconst Component = () =\u003e {\n\tif (!done) {\n\t\tthrow delay(3000).then(() =\u003e (done = true))\n\t} else {\n\t\tdone = false\n\t\treturn \u003cb\u003eHello, World!\u003c/b\u003e\n\t}\n}\n\nexport default new RouteReact(async () =\u003e {\n\treturn \u003cComponent /\u003e\n})\n```\n\nIn a browser, accessing [`http://localhost:8000`](http://localhost:8000) will show you “Loading…” for 3 seconds, and then “Hello, World!”.\n\n### Changing Suffix\n\nChanging the route filename suffix (`route` by default) is possible by `--suffix` when using the CLI and by `suffix` option when using the `Router` constructor. Although, there are some restrictions on the shape of suffixes:\n\n- Cannot be empty\n- Cannot contain slashes\n- Cannot start or end with dots\n\nThese are by design and will never be lifted. `routets` is made with the principle of least surprise; suffixes are technically required as Deno doesn't recognize a file named `.ts` to be a TypeScript module, while you must be freely able to use _any_ route file name for your own purpose, including the empty string.\n\nNotably, use of suffix allows you to place related modules like `*.test.ts` aside of routes.\n\n## Deploying to Deno Deploy\n\nBasically, `routets` uses non-statically-analyzeable dynamic imports to discover routes. This works well locally, but can be a problem if you want to get it to work with environments that don't support non-statically-analyzeable dynamic imports, such as [Deno Deploy](https://github.com/denoland/deploy_feedback/issues/433).\n\nFor this use case, you can use `routets --write serve.gen.ts` which generates an index module at the specified path (relative from the serving root) that does only statically-analyzeable dynamic import of routes. This module can directly be used as the entrypoint for Deno Deploy.\n\n## Difference from `fsrouter`\n\nThere exists a similar package [`fsrouter`](https://deno.land/x/fsrouter) which has quite the same UX overall, but slightly different in:\n\n- Suffix namespacing. `routets` uses namespaced filenames e.g. `greet.route.ts`, while `fsrouter` is just `greet.ts`.\n- Dynamic routing syntax. `routets` uses [URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) e.g. `:id.route.ts`, while `fsrouter` uses the [bracket syntax](https://github.com/justinawrey/fsrouter#dynamic-routes) e.g. `[id].ts`. Also, `routets` doesn't support [typed dynamic routes](https://github.com/justinawrey/fsrouter#typed-dynamic-routes).\n- JavaScript file extensions. `routets` doesn't allow `js` or `jsx`, while `fsrouter` does.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuhr%2Froutets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyuhr%2Froutets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuhr%2Froutets/lists"}