
A server-side web framework.
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.
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 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" },
})
noopA 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 https
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: "/",
})
CookieOptionsdomain, 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")
CacheOptionspublic, 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 using CF-Connecting-IP or X-Forwarded-ForrateLimit.memory() -- in-memory sliding window storehardened(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 guard
hsts(options?)Sets Strict-Transport-Security. Defaults: max-age=31536000; includeSubDomains.
| Option | Type | Default |
|---|---|---|
maxAge |
number |
31536000 |
includeSubDomains |
boolean |
true |
preload |
boolean |
false |
noSniffSets X-Content-Type-Options: nosniff.
frameGuard.deny / frameGuard.sameOriginSets 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") }
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." |
When route() is called it creates a Router and returns its route method. On each request:
RequestContext is created with request, env, url, bind, and dispatch.Container was provided, it is cloned and the context is bound into it.undefined value wins.bind(bindings)Merges key-value pairs into the current request's context. With a container, values are registered in the IoC container; without one, they're Object.assigned directly onto the context object. This is how middleware like cookies() and requestId() expose their state to downstream handlers.
dispatch(handler, middlewares)Runs a single handler through a middleware chain:
before hook runs in order. If any returns a value, it short-circuits as the response.undefined, dispatch returns undefined (no match).bind({ response }).after hook runs in order. If any returns a value, it replaces the response.Both bind and dispatch are exposed on the context, so handlers like prefix and pattern can create nested dispatch chains with their own middleware stacks.
When RouterOptions.container is provided (a @sigitex/bind Container), the router uses dependency injection instead of plain context objects:
bind() registers values in the container rather than mutating the context.invoke() calls container.call(handler) instead of handler(context), letting the container resolve handler parameters by type.Without a container, handlers receive the context object directly as their first argument.
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.