@openapi-spec/router
v0.8.0
Published
Fast HTTP router for OpenAPI path templates, with {name} and {+name} params, built for oRPC
Maintainers
Readme
@openapi-spec/router matches HTTP requests against OpenAPI path templates such as /planets/{id} and /files/{+path}, with no route syntax of its own to translate them into. It is built for how oRPC routes OpenAPI procedures: small, with no dependencies, and faster than rou3 on the route sets oRPC builds.
Usage
import { createPrefixMatcher, Router } from '@openapi-spec/router'
const router = new Router<string>()
router.add('GET', '/planets', 'list')
router.add('GET', '/planets/{id}', 'find')
router.add('GET', '/files/{+path}/raw', 'raw')
router.match('GET', '/planets/earth')
// { data: 'find', params: { id: 'earth' } }
router.match('GET', '/files/docs/a%20b.md/raw')
// { data: 'raw', params: { path: 'docs/a b.md' } }
router.match('GET', '/planets/')
// { data: 'list', params: undefined }
router.match('POST', '/planets')
// undefined
// Whether a request can reach a route under a prefix, such as one of a lazy router
const underUser = createPrefixMatcher('/users/{userId}')
underUser('/users/din/settings') // true
underUser('/projects/42') // falsematch takes the request method and its percent-encoded path, without the query string, as new URL(request.url).pathname gives it. Methods match exactly, so pass them in the case the routes use.
Path templates
{name}as a whole segment is a param. It matches one non-empty segment. Names are made of letters, digits,_, and-.{+name}as a whole segment is a catch-all param. It matches one or more segments, slashes included, and its value cannot be empty. A path can have one, and segments can follow it, as in/files/{+path}/raw. Adding a path with more than one throws aTypeError.- Any other text is literal, including braces that are not a whole segment, as in
/a/{id}.json, and characters other routers treat as syntax, such as:,*,?,(, and.. - Literal text matches however the route or the request percent-encodes it:
/cafématches/caf%C3%A9, and/v1^betamatches/v1%5Ebeta. A%2Fstays inside its segment. Text whose percent-encoding is malformed is compared as written. - Param values are percent-decoded. A catch-all param joins its decoded segments with
/. - One trailing slash is ignored, in routes and in requests. Other empty segments are kept:
/a//bdoes not match/a/b.
Priority
When more than one route matches, segments compare from left to right. At each segment a literal beats a param, which beats a catch-all param, and a route that fails further on falls back to the next candidate:
router.add('GET', '/users/me', 'me')
router.add('GET', '/users/{id}', 'user')
router.add('GET', '/users/{id}/posts', 'posts')
router.add('GET', '/{+path}', 'fallback')
router.match('GET', '/users/me') // 'me'
router.match('GET', '/users/m%65') // 'me'
router.match('GET', '/users/me/posts') // 'posts', with { id: 'me' }
router.match('GET', '/users/me/likes') // 'fallback'Catch-all params that start at the same segment compare the segments after them from the last one, so /files/{+path}/raw beats /files/{+path} for /files/a/raw. A route with the same method and segments as one added earlier, even with other param names, is ignored.
Performance
A request path without % that matches a route without params is found with one lookup. Other paths are split into segments once and walk a tree of literal, param, and catch-all children, so the work grows with the number of segments, not the number of routes. A tree that holds, at many segments in a row, both literals and params a request could take can fall back many times before it finds a match, as with any router that backtracks.
add keeps each route with params as written, and the tree is built as requests reach it: a method's first request that needs the tree adds each route's segments up to its first param, and the segments from each param on wait until a request reaches that point. A process that starts cold and serves a few requests pays only for the methods it serves and the params their requests reach.
Paths with params cost more than static ones, since each of their segments is sliced out and matched on its own. Radix-tree routers such as memoirist and find-my-way compare the path in place and can be faster on them.
Benchmarks compare it with rou3 on route sets like oRPC's. CodSpeed runs them on every pull request to catch regressions.
Sponsors
Like what we build over at middleapi? You can help keep it going through GitHub Sponsors or Open Collective. Every bit helps! 🚀
Organization Sponsors
Sponsors
Backers
With thanks to 40 past sponsors who helped get openapi-spec here.
Open Source Programs
middleapi is supported by these open source programs:
License
Distributed under the MIT License. See LICENSE for more information.
