- TypeScript 99.1%
- JavaScript 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .husky | ||
| src | ||
| .gitignore | ||
| bun.lock | ||
| bunfig.toml | ||
| commitlint.config.ts | ||
| LICENSE | ||
| oxfmt.config.ts | ||
| oxlint.config.ts | ||
| package.json | ||
| README.md | ||
| regime.config.json | ||
| release.config.cjs | ||
| route.code-workspace | ||
| tsconfig.base.json | ||
| tsconfig.bun.json | ||
| tsconfig.cloudflare.json | ||
| tsconfig.config.json | ||
| tsconfig.json | ||
| tsconfig.src.json | ||
| tsconfig.test.json | ||
@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 https
Middleware
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 guard
hsts(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.