@sigitex/route
v1.0.5
Published
A server-side web framework.
Readme
@sigitex/route
A server-side web framework.
bun add @sigitex/route
Note: This package currently exports TypeScript sources directly. A TypeScript-compatible runtime or bundler (Bun, etc.) is required.
Quick Start
import { route, get, post, prefix, cors, hardened, cookies } from "@sigitex/route"
import { bun } from "@sigitex/route/bun"
const fetch = route(
bun({ assets: "./public" }),
get("/health", () => ({ status: "ok" })),
prefix("/api/", [cors(), cookies(), hardened()],
get("/users", async () => {
return Response.json(await getUsers())
}),
get("/users/:id", async ({ params }: { params: { id: string } }) => {
return Response.json(await getUser(params.id))
}),
post("/users", async ({ request }: { request: Request }) => {
const body = await request.json()
return Response.json(await createUser(body))
}),
),
)
Bun.serve({ fetch })On Cloudflare Workers:
import { route, get } from "@sigitex/route"
import { cloudflare } from "@sigitex/route/cloudflare"
export default {
fetch: route(
cloudflare(),
get("/hello", () => ({ hello: "world" })),
),
}route(...handlers)
Creates a fetch function (request: Request, env: Env) => Promise<Response> from a list of handlers. Handlers are tried in order; the first to return a value produces the response. If none match, a 404 is returned.
const fetch = route(handler1, handler2, handler3)An optional RouterOptions object can be passed as the first argument:
const fetch = route({ container, middlewares: [cors()] }, handler1, handler2)Falsy values (null, undefined, false, 0) are silently ignored, allowing conditional handlers:
route(
isDev && get("/debug", debugHandler),
get("/", homeHandler),
)RouterOptions
| Option | Type | Description |
| ------------- | ----------------- | ------------------------------------------------------ |
| container | Container | A @sigitex/bind IoC container for dependency injection |
| middlewares | RouteMiddleware[] | Global middlewares applied to every dispatched handler |
Handlers
Handlers are functions that receive a context object and return a Response, a JSON-serializable value, or undefined to skip.
get(path, handler, ...middlewares)
Matches GET requests against path. Path parameters use regexparam syntax (:param, *).
get("/users/:id", ({ params }: { params: { id: string } }) => {
return Response.json({ id: params.id })
})post(path, handler, ...middlewares)
Matches POST requests.
put(path, handler, ...middlewares)
Matches PUT requests.
del(path, handler, ...middlewares)
Matches DELETE requests.
patch(path, handler, ...middlewares)
Matches PATCH requests.
pattern(method, path, handler, ...middlewares)
Generic version -- pass null as the method to match any HTTP method.
pattern(null, "/any-method/:id", handler)
pattern("GET", "/explicit", handler)prefix(prefix, ...handlers)
Groups handlers under a URL prefix. The prefix is stripped from the URL before child handlers see it.
prefix("/api/v1/",
get("/users", listUsers), // matches /api/v1/users
get("/posts", listPosts), // matches /api/v1/posts
)Accepts an optional middlewares array as the second argument:
prefix("/api/", [cors(), bodyLimit()],
post("/upload", uploadHandler),
)mount(fetchFn)
Wraps a standard (request: Request) => Promise<Response> function as a handler. Useful for mounting sub-applications or external fetch handlers.
mount(subApp.fetch)assets()
Serves static files via the platform's Assets binding. Returns undefined on 404 so subsequent handlers can match.
route(bun(), assets(), get("/", homeHandler))app(routes)
Serves index.html for paths matching a client-side RouteTree. Intended for single-page applications where the client handles routing.
app({
users: "/users",
user: "/users/:id",
settings: { general: "/settings/general" },
})noop
A handler that does nothing and returns undefined. Used internally by middleware composition.
use(middlewares, ...handlers)
Applies a set of middlewares to a group of handlers without creating a prefix.
use([cors(), cookies()],
get("/a", handlerA),
get("/b", handlerB),
)filter(predicate)
Higher-order function that conditionally runs a handler based on a predicate.
const onlyJson = filter(({ request }) =>
request.headers.get("Accept")?.includes("application/json") ?? false
)
onlyJson(get("/data", dataHandler))https()
Redirects HTTP requests to HTTPS with a 301.
route(https(), get("/", homeHandler))www(options)
Redirects non-www requests to the www subdomain.
www({ secure: true }) // also upgrades to httpsMiddleware
Middlewares are objects with optional before and after hooks. before runs before the handler; after runs after. Either can return a Response to short-circuit.
const myMiddleware: RouteMiddleware = {
before: ({ request, bind }) => {
bind({ startTime: Date.now() })
},
after: ({ response, startTime }) => {
response.headers.set("X-Duration", String(Date.now() - startTime))
},
}cors(options?)
Handles CORS preflight and response headers.
| Option | Type | Default |
| --------------- | ------------------------------------------------- | ---------- |
| origin | string \| string[] \| (origin: string) => boolean | "*" |
| methods | string[] | all standard |
| allowHeaders | string[] | mirrors request |
| exposeHeaders | string[] | -- |
| credentials | boolean | false |
| maxAge | number | -- |
cookies()
Parses request cookies and collects Set-Cookie headers on the response. Binds a Cookies object to context:
cookies.get("session") // read
cookies.set("session", token, { // write
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 86400,
path: "/",
})CookieOptions
domain, expires, httpOnly, maxAge, path, sameSite ("strict" | "lax" | "none"), secure.
bodyLimit(options?)
Rejects requests exceeding a body size or with disallowed content types.
| Option | Type | Default |
| -------------- | ---------- | --------- |
| maxSize | number | 1 MB |
| contentTypes | string[] | any |
cache(options)
Sets Cache-Control (and optionally Vary) headers on responses.
cache({ public: true, maxAge: 3600 })
cache("no-store")CacheOptions
public, private, maxAge, sMaxAge, noCache, noStore, mustRevalidate, proxyRevalidate, immutable, staleWhileRevalidate, staleIfError, vary.
csp(options)
Sets Content-Security-Policy headers. Supports automatic nonce generation via the CSP.nonce symbol -- the nonce is bound to context as cspNonce.
import { CSP } from "@sigitex/route"
csp({
defaultSrc: [CSP.self],
scriptSrc: [CSP.self, CSP.nonce],
styleSrc: [CSP.self, CSP.unsafeInline],
imgSrc: [CSP.self, CSP.data],
reportOnly: true,
})csrf(options?)
Double-submit cookie CSRF protection. Requires cookies() in the middleware stack.
| Option | Type | Default |
| --------- | ---------- | -------------------------------- |
| cookie | string | "csrf-token" |
| header | string | "X-CSRF-Token" |
| methods | string[] | POST, PUT, PATCH, DELETE |
rateLimit(options?)
IP-based rate limiting with pluggable storage.
| Option | Type | Default |
| --------- | ------------------ | ------------------------ |
| window | number (seconds) | 60 |
| max | number | 100 |
| key | (ctx) => string | rateLimit.ip |
| store | RateLimitStore | rateLimit.memory() |
| headers | boolean | true |
Built-in helpers:
rateLimit.ip-- key extractor usingCF-Connecting-IPorX-Forwarded-ForrateLimit.memory()-- in-memory sliding window store
hardened(options?)
Convenience bundle applying noSniff, frameGuard, referrerPolicy, and hsts. Any can be disabled:
hardened() // all defaults
hardened({ hsts: false }) // skip HSTS
hardened({ frameGuard: "sameOrigin" }) // override frame guardhsts(options?)
Sets Strict-Transport-Security. Defaults: max-age=31536000; includeSubDomains.
| Option | Type | Default |
| ------------------- | --------- | ----------- |
| maxAge | number | 31536000 |
| includeSubDomains | boolean | true |
| preload | boolean | false |
noSniff
Sets X-Content-Type-Options: nosniff.
frameGuard.deny / frameGuard.sameOrigin
Sets X-Frame-Options.
referrerPolicy.*
Pre-built policies: noReferrer, noReferrerWhenDowngrade, origin, originWhenCrossOrigin, sameOrigin, strictOrigin, strictOriginWhenCrossOrigin, unsafeUrl.
requestId(options?)
Reads or generates a request ID, binds it to context as requestId, and echoes it on the response.
| Option | Type | Default |
| ---------- | -------------- | -------------------- |
| header | string | "X-Request-Id" |
| generate | () => string | crypto.randomUUID |
setHeader(header, value)
Sets a header on every response. Returns a ResponseHandler (use as an after hook).
{ after: setHeader("X-Powered-By", "sigitex") }Errors
Error classes that can be thrown from handlers:
| Class | Status | Default Message |
| ------------------ | ------ | ------------------------ |
| RouterError | any | -- |
| NotFound | 404 | "Not found." |
| MethodNotAllowed | 405 | "Method not allowed." |
| InvalidRequest | 400 | "Invalid request." |
| ServerError | -- | "Internal error." |
Internals
Request Lifecycle
When route() is called it creates a Router and returns its route method. On each request:
- A single request context is created with
request,env,url,bind, anddispatch. - If a
Containerwas provided, it is cloned once and that clone is used for the whole request. - Global middlewares wrap the candidate loop once. Handlers are tried in order; the first non-
undefinedresult wins. - If no handler matches, a JSON 404 is returned. Uncaught errors produce a JSON 500.
bind(bindings)
Merges key-value pairs into the request context. Bindings stick for the rest of the request, including across nested dispatch() calls. With a container, values are registered on the per-request clone; without one, they're Object.assigned onto the context object. This is how middleware like cookies(), requestId(), and a session user expose state to downstream handlers.
Match-local keys are an exception at the call site: prefix() save/restores url, and pattern() save/restores params, so fallthrough does not leak a stripped path or stale params to siblings.
dispatch(handler, middlewares)
Runs a single handler through a middleware chain on the same request context (no binding scope stack):
- Each middleware's
beforehook runs in order. If any returns a value, it short-circuits as the response. - The handler is invoked. If it returns
undefined, dispatch returnsundefined(no match) and after-hooks do not run. - The response is bound via
bind({ response }). - Each entered middleware's
afterhook runs in reverse registration order. A returned value replaces the response. - The final response is returned.
Both bind and dispatch are exposed on the context so use / prefix / pattern can nest middleware stacks without losing request bindings.
Container Integration
When RouterOptions.container is provided (a @sigitex/bind Container), the router uses dependency injection instead of plain context objects:
- The container is cloned once per request so concurrent requests don't share state.
bind()registers values on that clone rather than mutating a shared container.- Handlers are invoked via
container.call(handler), so the container resolves handler parameters by name.
Without a container, handlers receive the context object directly as their first argument.
Response Coercion
Return values from handlers are coerced: Response instances pass through as-is; anything else is JSON.stringifyed into a Response. Returning undefined signals "no match" and the next handler is tried.
