imperouter
v0.9.0
Published
Imperative router for hybrid SSR+SPA apps. Uses standard `Request` and `URL` APIs. Extremely simple and flexible.
Downloads
3
Maintainers
Readme
Overview
Imperative router for hybrid SSR+SPA apps. Uses the standard built-in Request
and URL
APIs. Works as-is in Deno and browsers. Requires Request
and Response
polyfills in Node.
Similar to the Go router rout
.
Features:
- Simple, expressive router for SSR+SPA.
- Only functions of
Request
,Response
, andURL
. No added abstractions. - Imperative control.
- Freedom to route by method, path, or both.
- Abstract, usable for server-side routing or with any UI library.
- Uses regexps with named capture groups. Also allows strings for exact matching. No custom string-based dialects.
Tiny, dependency-free, single file, native module.
TOC
Why
Instead 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.
Imperouter uses imperative, procedural control flow. It does not have, and does not need, any kind of "middleware" or "interceptors".
Imperouter uses regexps with named capture groups. It also allows strings for exact matching. No custom string-based dialects, no surprises.
Imperouter 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.
Usage
Install with NPM, or import by URL:
npm i -E imperouter
import * as r from 'imperouter'
import * as r from 'https://cdn.jsdelivr.net/npm/[email protected]/imperouter.mjs'
Example with Deno:
import * as r from 'imperouter'
function respond(event) {
const req = new r.Req(event.request)
event.respondWith(response(req))
}
function response(req) {
return (
r.preflight(req) ||
r.sub(req, /^[/]api(?:[/]|$)/, apiRoutes) ||
r.sub(req, /(?:)/, pageRoutes)
)
}
// Because of `sub`, if there's no match, this is 404.
async function apiRoutes(req) {
return cors(await (
r.methods(req, `/api/posts`, postRoot) ||
r.methods(req, /^[/]api[/]posts[/](?<id>[^/]+)$/, postById)
))
}
// Because of `methods`, if there's no match, this is 405.
function postRoot(req) {
return (
r.get(req, `/api/posts`, postFeed) ||
r.post(req, `/api/posts`, postCreate)
)
}
// Because of `methods`, if there's no match, this is 405.
function postById(req) {
return (
r.get(req, /^[/]api[/]posts[/](?<id>[^/]+)$/, postGet) ||
r.post(req, /^[/]api[/]posts[/](?<id>[^/]+)$/, postUpdate)
)
}
// Because of `sub`, if there's no match, this is 404.
// But a website must have its own 404 route with an HTML page.
function pageRoutes(req) {
return (
r.get(req, `/posts`, posts) ||
r.get(req, /^[/]posts[/](?<id>[^/])$/, post) ||
r.get(req, /(?:)/, notFound)
)
}
function postFeed(req) {return new Response(`url: ${req.url}`)}
function postCreate(req) {return new Response(`url: ${req.url}`)}
function postGet(req, {id}) {return new Response(`url: ${req.url}`)}
function postUpdate(req, {id}) {return new Response(`url: ${req.url}`)}
function posts(req) {return new Response(`url: ${req.url}`)}
function post(req, {id}) {return new Response(`url: ${req.url}`)}
function notFound(req) {
return new Response(`not found: ${req.url}`, {status: 404})
}
function cors(res) {
res.headers.set('access-control-allow-credentials', 'true')
res.headers.set('access-control-allow-headers', 'cache-control, content-type')
res.headers.set('access-control-allow-methods', 'OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE')
res.headers.set('access-control-allow-origin', '*')
return res
}
API
All examples imply an import:
import * as r from 'imperouter'
class Req extends Request
Optional subclass of standard Request
. Adds req.URL
which can slightly improve routing performance by avoiding repeated parsing.
Make 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.
Completely optional. All routing functions work on standard Request
.
// In Deno.
req = new r.Req(req)
// In browsers, for pushstate routing.
const req = new r.Req(window.location)
property req.URL
Instance of 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
.
function preflight(req, fun = empty)
If req.method
matches HEAD
or OPTIONS
, generate a response, default empty
. Otherwise, return nil. See Usage. You can also pass a different function:
r.preflight(req, preflight) || otherRoutes(req)
function preflight(req) {return new Response(`ok to proceed`)}
function sub(req, pat, fun)
If 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.
r.sub(req, /^[/]api(?:[/]|$)/, apiRoutes) ||
r.sub(req, /(?:)/, pageRoutes)
function methods(req, pat, fun)
Similar to sub
, 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.
r.methods(req, `/api/posts`, postRoot) || otherRoutes(req)
function postRoot(req) {
return (
r.get(req, `/api/posts`, postFeed) ||
r.post(req, `/api/posts`, postCreate)
)
}
function method(req, method, pat, fun)
If 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.
imperouter
has shortcuts such as get
. You should only need method
for routing on unusual or custom HTTP methods.
To match any path, use /(?:)/
:
r.method(req, 'someHttpMethod', /(?:)/, someFun) || otherRoutes(req)
function any(req, pat, fun)
If pathname of req.url
matches pat
, then fun(req, groups)
, where groups
are named capture groups from the pattern. Otherwise nil.
// 404 on unknown file requests.
r.any(req, /[.]\w+$/, r.notFound) || otherRoutes(req)
function get(req, pat, fun)
Shortcut for r.method(req, r.GET, fun)
. See method
and Usage.
function head(req, pat, fun)
Shortcut for r.method(req, r.HEAD, fun)
. See method
and Usage.
function options(req, pat, fun)
Shortcut for r.method(req, r.OPTIONS, fun)
. See method
and Usage.
function post(req, pat, fun)
Shortcut for r.method(req, r.POST, fun)
. See method
and Usage.
function put(req, pat, fun)
Shortcut for r.method(req, r.PUT, fun)
. See method
and Usage.
function patch(req, pat, fun)
Shortcut for r.method(req, r.PATCH, fun)
. See method
and Usage.
function del(req, pat, fun)
Shortcut for r.method(req, r.DELETE, fun)
. See method
and Usage.
function notFound(req)
Shortcut for an extremely simple 404 response. Fallback in sub
.
function notAllowed(req)
Shortcut for an extremely simple 405 response. Fallback in methods
.
function empty()
Shortcut for new Response()
, which is a valid empty response. Default in preflight
.
Undocumented
Some useful constants and functions are exported but undocumented to reduce doc bloat. Check the source or the type definition.
Do and Don't
Haven't ran benchmarks yet, but you should probably:
- Use
Req
to avoid repeated URL parsing. - Avoid large flat tables. Instead, structure your routes as trees.
- Avoid local closures. Instead, define route handlers statically.
Do:
function response(req) {
// Avoids repeated URL parsing: `req.URL` is pre-parsed.
req = new r.Req(req)
return (
// Grouped-up. If there's no match, the path is tested only once,
// regardless of how many sub-routes it has.
r.get(req, /^[/]posts(?:[/]|$)/, postRoutes) ||
r.get(req, /(?:)/, notFound)
)
}
function postRoutes(req) {
return (
r.get(req, /^[/]posts$/, posts) ||
r.get(req, /^[/]posts[/](?<id>[^/])$/, post)
)
}
// Statically defined functions, no closures.
function posts(req) {}
function post(req, {id}) {}
function notFound(req) {}
Don't:
// Single flat table. Multiple closures.
function response(req) {
return (
r.get(req, /^[/]posts$/, req => {}) ||
r.get(req, /^[/]posts[/](?<id>[^/])$/, (req, {id}) => {}) ||
r.get(req, /(?:)/, req => {})
)
}
Changelog
0.9.0
Cleanup: dropped URL-related utils. Use https://github.com/mitranim/ur for that.
0.8.3
Minor bugfixes in .d.ts
definition.
0.8.2
Support string patterns for exact pathname matching.
0.8.1
Allow imperouter.d.ts
in .npmignore
.
0.8.0
- Breaking: converted
Router
methods to functions, removedRouter
. - Added TypeScript definitions.
0.7.0
Breaking API revision: Request
-based routing for SSR+SPA.
0.6.1
Bugfixes for query mutations. This affects all query-related functions.
0.6.0
Breaking API revision: removed match
, revised find
.
find
no longer deals with URL
objects. It takes a plain string, runs routes against it, and returns {route, groups}
.
Route regexp must be route.reg
, rather than route.path
. Imperouter attaches no special meaning to the string passed to it.
0.5.1
Allow routes to be non-plain objects.
0.5.0
Super breaking!
No more UI adapters.
No more 'history'
integration.
Using the native URL
interface instead of 'history'
's "location" dicts.
Added the previously-missing license (unlicense).
0.4.0
Now provided only as native JS modules (.mjs
).
0.3.1
Bugfix for Preact: fixed incorrect unwrapping of context.history
.
0.3.0
Minor but potentially breaking changes:
<Link>
withtarget='_blank'
acts like a standard<a>
, does not trigger pushstate navigationencodeQuery
no longer prepends?
params
now inherit fromnull
rather thanObject.prototype
Added feature:
- support ES2018 regexp named capture groups
0.2.0
Added React adapter.
License
https://unlicense.org
Misc
I'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