{"id":24083317,"url":"https://github.com/mitranim/imperouter","last_synced_at":"2025-08-23T01:12:35.884Z","repository":{"id":57272938,"uuid":"141924169","full_name":"mitranim/imperouter","owner":"mitranim","description":"Imperative router for hybrid SSR+SPA JS apps. Uses the standard `Request` and `URL` interfaces with no added junk.","archived":false,"fork":false,"pushed_at":"2022-02-08T08:32:06.000Z","size":91,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-15T17:30:36.840Z","etag":null,"topics":["router","routing"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mitranim.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-22T19:21:17.000Z","updated_at":"2023-08-01T09:09:26.000Z","dependencies_parsed_at":"2022-08-25T01:12:18.995Z","dependency_job_id":null,"html_url":"https://github.com/mitranim/imperouter","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/mitranim/imperouter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fimperouter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fimperouter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fimperouter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fimperouter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mitranim","download_url":"https://codeload.github.com/mitranim/imperouter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fimperouter/sbom","scorecard":{"id":650478,"data":{"date":"2025-08-11","repo":{"name":"github.com/mitranim/imperouter","commit":"af12382192611d1d9fc864bc4a4f334d37d429dc"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":0,"reason":"Found 0/19 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-21T13:23:32.492Z","repository_id":57272938,"created_at":"2025-08-21T13:23:32.492Z","updated_at":"2025-08-21T13:23:32.492Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271727843,"owners_count":24810563,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-22T02:00:08.480Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["router","routing"],"created_at":"2025-01-09T23:56:21.451Z","updated_at":"2025-08-23T01:12:35.859Z","avatar_url":"https://github.com/mitranim.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Overview\n\nImperative router for hybrid SSR+SPA apps. Uses the standard built-in [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) APIs. Works as-is in [Deno](https://deno.land) and browsers. Requires `Request` and `Response` polyfills in Node.\n\nSimilar to the Go router [`rout`](https://github.com/mitranim/rout).\n\nFeatures:\n\n  * Simple, expressive router for SSR+SPA.\n  * Only functions of `Request`, `Response`, and `URL`. No added abstractions.\n  * Imperative control.\n  * Freedom to route by method, path, or both.\n  * Abstract, usable for server-side routing or with any UI library.\n  * Uses regexps with named capture groups. Also allows strings for _exact_ matching. No custom string-based dialects.\n\nTiny, dependency-free, single file, native module.\n\n## TOC\n\n* [Overview](#overview)\n* [Why](#why)\n* [Usage](#usage)\n* [API](#api)\n  * [`class Req extends Request`](#class-req-extends-request)\n    * [`property req.URL`](#property-requrl)\n  * [`function preflight`](#function-preflightreq-fun--empty)\n  * [`function sub`](#function-subreq-pat-fun)\n  * [`function methods`](#function-methodsreq-pat-fun)\n  * [`function method`](#function-methodreq-method-pat-fun)\n  * [`function any`](#function-anyreq-pat-fun)\n  * [`function get`](#function-getreq-pat-fun)\n  * [`function head`](#function-headreq-pat-fun)\n  * [`function options`](#function-optionsreq-pat-fun)\n  * [`function post`](#function-postreq-pat-fun)\n  * [`function put`](#function-putreq-pat-fun)\n  * [`function patch`](#function-patchreq-pat-fun)\n  * [`function del`](#function-delreq-pat-fun)\n  * [`function notFound`](#function-notfoundreq)\n  * [`function notAllowed`](#function-notallowedreq)\n  * [`function empty`](#function-empty)\n  * [Undocumented](#undocumented)\n* [Do and Don't](#do-and-dont)\n* [Changelog](#changelog)\n* [Misc](#misc)\n\n## Why\n\nInstead of replacing or wrapping standard interfaces, Imperouter lets you work _directly_ with the built-in `Request` and `URL` APIs, extending what you can do with them.\n\nImperouter uses imperative, procedural control flow. It does not have, and _does not need_, any kind of \"middleware\" or \"interceptors\".\n\nImperouter uses regexps with named capture groups. It also allows strings for _exact_ matching. No custom string-based dialects, no surprises.\n\nImperouter does not use any URL mounting, joining, or rewriting. You always match the full pathname, and receive the full URL, including the origin. This allows you to understand full routes on a glance, search them in your editor, and benefit from the standard `URL` interface.\n\n## Usage\n\nInstall with NPM, or import by URL:\n\n```sh\nnpm i -E imperouter\n```\n\n```js\nimport * as r from 'imperouter'\n\nimport * as r from 'https://cdn.jsdelivr.net/npm/imperouter@0.8.3/imperouter.mjs'\n```\n\nExample with Deno:\n\n```js\nimport * as r from 'imperouter'\n\nfunction respond(event) {\n  const req = new r.Req(event.request)\n  event.respondWith(response(req))\n}\n\nfunction response(req) {\n  return (\n    r.preflight(req) ||\n    r.sub(req, /^[/]api(?:[/]|$)/, apiRoutes) ||\n    r.sub(req, /(?:)/,             pageRoutes)\n  )\n}\n\n// Because of `sub`, if there's no match, this is 404.\nasync function apiRoutes(req) {\n  return cors(await (\n    r.methods(req, `/api/posts`,                      postRoot) ||\n    r.methods(req, /^[/]api[/]posts[/](?\u003cid\u003e[^/]+)$/, postById)\n  ))\n}\n\n// Because of `methods`, if there's no match, this is 405.\nfunction postRoot(req) {\n  return (\n    r.get(req,  `/api/posts`, postFeed) ||\n    r.post(req, `/api/posts`, postCreate)\n  )\n}\n\n// Because of `methods`, if there's no match, this is 405.\nfunction postById(req) {\n  return (\n    r.get(req,  /^[/]api[/]posts[/](?\u003cid\u003e[^/]+)$/, postGet) ||\n    r.post(req, /^[/]api[/]posts[/](?\u003cid\u003e[^/]+)$/, postUpdate)\n  )\n}\n\n// Because of `sub`, if there's no match, this is 404.\n// But a website must have its own 404 route with an HTML page.\nfunction pageRoutes(req) {\n  return (\n    r.get(req, `/posts`,                   posts) ||\n    r.get(req, /^[/]posts[/](?\u003cid\u003e[^/])$/, post)  ||\n    r.get(req, /(?:)/,                     notFound)\n  )\n}\n\nfunction postFeed(req)         {return new Response(`url: ${req.url}`)}\nfunction postCreate(req)       {return new Response(`url: ${req.url}`)}\nfunction postGet(req, {id})    {return new Response(`url: ${req.url}`)}\nfunction postUpdate(req, {id}) {return new Response(`url: ${req.url}`)}\nfunction posts(req)            {return new Response(`url: ${req.url}`)}\nfunction post(req, {id})       {return new Response(`url: ${req.url}`)}\n\nfunction notFound(req)         {\n  return new Response(`not found: ${req.url}`, {status: 404})\n}\n\nfunction cors(res) {\n  res.headers.set('access-control-allow-credentials', 'true')\n  res.headers.set('access-control-allow-headers', 'cache-control, content-type')\n  res.headers.set('access-control-allow-methods', 'OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE')\n  res.headers.set('access-control-allow-origin', '*')\n  return res\n}\n```\n\n## API\n\nAll examples imply an import:\n\n```js\nimport * as r from 'imperouter'\n```\n\n### `class Req extends Request`\n\nOptional subclass of standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request). Adds `req.URL` which can slightly improve routing performance by avoiding repeated parsing.\n\nMake it from an existing request (in Deno for SSR), or from scratch (in browsers for pushstate). You can also subclass `Req`, adding your own properties or methods.\n\nCompletely optional. All routing functions work on standard `Request`.\n\n```js\n// In Deno.\nreq = new r.Req(req)\n\n// In browsers, for pushstate routing.\nconst req = new r.Req(window.location)\n```\n\n#### `property req.URL`\n\nInstance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) created from `req.url`. All regexp-based functions in `imperouter` match against `req.URL.pathname` if available, which can slightly improve performance. Otherwise, they parse `req.url`.\n\n### `function preflight(req, fun = empty)`\n\nIf `req.method` matches `HEAD` or `OPTIONS`, generate a response, default [`empty`](#function-empty). Otherwise, return nil. See [Usage](#usage). You can also pass a different function:\n\n```js\nr.preflight(req, preflight) || otherRoutes(req)\n\nfunction preflight(req) {return new Response(`ok to proceed`)}\n```\n\n### `function sub(req, pat, fun)`\n\nIf pathname of `req.url` matches `pat`, _then_ `fun(req) || notFound(req)`, async if needed. Otherwise nil. Ensures that once we match a branch, we definitely return a response, falling back on 404 \"not found\", and without trying any other branches. See [Usage](#usage).\n\n```js\nr.sub(req, /^[/]api(?:[/]|$)/, apiRoutes) ||\nr.sub(req, /(?:)/,             pageRoutes)\n```\n\n### `function methods(req, pat, fun)`\n\nSimilar to [`sub`](#function-subreq-pat-fun), but 405. If pathname of `req.url` matches `pat`, _then_ `fun(req) || notAllowed(req)`, async if needed. Otherwise nil. Ensures that once we match a branch, we definitely return a response, falling back on 405 \"method not allowed\", and without trying any other branches. See [Usage](#usage).\n\n```js\nr.methods(req, `/api/posts`, postRoot) || otherRoutes(req)\n\nfunction postRoot(req) {\n  return (\n    r.get(req, `/api/posts`, postFeed) ||\n    r.post(req, `/api/posts`, postCreate)\n  )\n}\n```\n\n### `function method(req, method, pat, fun)`\n\nIf `req.method` matches `method` _and_ pathname of `req.url` matches `pat`, _then_ `fun(req, groups)`, where `groups` are named capture groups from the pattern. Otherwise nil.\n\n`imperouter` has shortcuts such as [`get`](#function-getreq-pat-fun). You should only need `method` for routing on unusual or custom HTTP methods.\n\nTo match any path, use `/(?:)/`:\n\n```js\nr.method(req, 'someHttpMethod', /(?:)/, someFun) || otherRoutes(req)\n```\n\n### `function any(req, pat, fun)`\n\nIf pathname of `req.url` matches `pat`, _then_ `fun(req, groups)`, where `groups` are named capture groups from the pattern. Otherwise nil.\n\n```js\n// 404 on unknown file requests.\nr.any(req, /[.]\\w+$/, r.notFound) || otherRoutes(req)\n```\n\n### `function get(req, pat, fun)`\n\nShortcut for `r.method(req, r.GET, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function head(req, pat, fun)`\n\nShortcut for `r.method(req, r.HEAD, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function options(req, pat, fun)`\n\nShortcut for `r.method(req, r.OPTIONS, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function post(req, pat, fun)`\n\nShortcut for `r.method(req, r.POST, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function put(req, pat, fun)`\n\nShortcut for `r.method(req, r.PUT, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function patch(req, pat, fun)`\n\nShortcut for `r.method(req, r.PATCH, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function del(req, pat, fun)`\n\nShortcut for `r.method(req, r.DELETE, fun)`. See [`method`](#function-methodreq-method-pat-fun) and [Usage](#usage).\n\n### `function notFound(req)`\n\nShortcut for an extremely simple 404 response. Fallback in [`sub`](#function-subreq-pat-fun).\n\n### `function notAllowed(req)`\n\nShortcut for an extremely simple 405 response. Fallback in [`methods`](#function-methodsreq-pat-fun).\n\n### `function empty()`\n\nShortcut for `new Response()`, which is a valid empty response. Default in [`preflight`](#function-preflightreq-fun--empty).\n\n### Undocumented\n\nSome useful constants and functions are exported but undocumented to reduce doc bloat. Check the [source](imperouter.mjs) or the [type definition](imperouter.d.ts).\n\n## Do and Don't\n\nHaven't ran benchmarks yet, but you should probably:\n\n* Use [`Req`](#class-req-extends-request) to avoid repeated URL parsing.\n* Avoid large flat tables. Instead, structure your routes as trees.\n* Avoid local closures. Instead, define route handlers statically.\n\nDo:\n\n```js\nfunction response(req) {\n  // Avoids repeated URL parsing: `req.URL` is pre-parsed.\n  req = new r.Req(req)\n\n  return (\n    // Grouped-up. If there's no match, the path is tested only once,\n    // regardless of how many sub-routes it has.\n    r.get(req, /^[/]posts(?:[/]|$)/, postRoutes) ||\n    r.get(req, /(?:)/, notFound)\n  )\n}\n\nfunction postRoutes(req) {\n  return (\n    r.get(req, /^[/]posts$/,               posts) ||\n    r.get(req, /^[/]posts[/](?\u003cid\u003e[^/])$/, post)\n  )\n}\n\n// Statically defined functions, no closures.\nfunction posts(req) {}\nfunction post(req, {id}) {}\nfunction notFound(req) {}\n```\n\nDon't:\n\n```js\n// Single flat table. Multiple closures.\nfunction response(req) {\n  return (\n    r.get(req, /^[/]posts$/,               req =\u003e {})         ||\n    r.get(req, /^[/]posts[/](?\u003cid\u003e[^/])$/, (req, {id}) =\u003e {}) ||\n    r.get(req, /(?:)/,                     req =\u003e {})\n  )\n}\n```\n\n## Changelog\n\n### `0.9.0`\n\nCleanup: dropped URL-related utils. Use https://github.com/mitranim/ur for that.\n\n### `0.8.3`\n\nMinor bugfixes in `.d.ts` definition.\n\n### `0.8.2`\n\nSupport string patterns for _exact_ pathname matching.\n\n### `0.8.1`\n\nAllow `imperouter.d.ts` in `.npmignore`.\n\n### `0.8.0`\n\n* Breaking: converted `Router` methods to functions, removed `Router`.\n* Added TypeScript definitions.\n\n### `0.7.0`\n\nBreaking API revision: `Request`-based routing for SSR+SPA.\n\n### `0.6.1`\n\nBugfixes for query mutations. This affects all query-related functions.\n\n### `0.6.0`\n\nBreaking API revision: removed `match`, revised `find`.\n\n`find` no longer deals with `URL` objects. It takes a plain string, runs routes against it, and returns `{route, groups}`.\n\nRoute regexp must be `route.reg`, rather than `route.path`. Imperouter attaches no special meaning to the string passed to it.\n\n### `0.5.1`\n\nAllow routes to be non-plain objects.\n\n### `0.5.0`\n\nSuper breaking!\n\nNo more UI adapters.\n\nNo more `'history'` integration.\n\nUsing the native `URL` interface instead of `'history'`'s \"location\" dicts.\n\nAdded the previously-missing license (unlicense).\n\n### `0.4.0`\n\nNow provided only as native JS modules (`.mjs`).\n\n### `0.3.1`\n\nBugfix for Preact: fixed incorrect unwrapping of `context.history`.\n\n### `0.3.0`\n\nMinor but potentially breaking changes:\n\n* `\u003cLink\u003e` with `target='_blank'` acts like a standard `\u003ca\u003e`, does not trigger pushstate navigation\n* `encodeQuery` no longer prepends `?`\n* `params` now inherit from `null` rather than `Object.prototype`\n\nAdded feature:\n\n* support ES2018 regexp named capture groups\n\n### `0.2.0`\n\nAdded React adapter.\n\n## License\n\nhttps://unlicense.org\n\n## Misc\n\nI'm receptive to suggestions. If this library _almost_ satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fimperouter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmitranim%2Fimperouter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fimperouter/lists"}