How Instatic Handles Server-Side Routing: Ordered Handler Patterns in Bun
Instatic implements server-side routing through a lightweight, hand-rolled router in server/router.ts that iterates over an ordered array of RouteHandler functions, returning the first non-null Response to process incoming requests on its Bun-based server.
Instatic, an open-source static site generator from CoreBunch, eschews traditional heavyweight frameworks in favor of a minimal, high-performance routing layer built specifically for Bun. The server-side routing architecture centers on a simple dispatcher pattern where an immutable sequence of handler functions competes to generate responses. This design prioritizes explicit execution order and namespace isolation over complex pattern-matching algorithms.
The RouteHandler Contract and Ordered Routing Table
At the heart of Instatic's server-side routing lies the RouteHandler type definition found in server/router.ts (lines 46-52). This contract defines a function that receives the incoming Request, runtime context, a parsed URL object, and the pathname string, returning either a Response or null.
RouteHandler Function Signature
Each handler must conform to this asynchronous signature:
type RouteHandler = (
req: Request,
runtime: ServerRuntime,
url: URL,
pathname: string,
) => Promise<Response | null>;
When a handler returns null, the dispatcher immediately proceeds to the next candidate. When a handler returns a Response, that response is sent directly to the client and the iteration stops.
Immutable Route Registration
Routes are registered in a readonly RouteHandler[] array declared in server/router.ts (lines 63-90). This array is ordered from most specific to most generic, ensuring that specialized endpoints like /_instatic/css/ or /admin/api/ are evaluated before the catch-all public page resolver. Adding a new endpoint requires only a single-line edit to this immutable table.
Request Dispatch Flow in server/router.ts
Entry Point and URL Parsing
The handleServerRequest function serves as the single entry point for all HTTP requests. It performs initial URL parsing using new URL(req.url) to extract the pathname before entering the dispatch loop.
The Dispatcher Loop
The core dispatch logic iterates through the routes array, awaiting each handler in sequence:
// Conceptual implementation from server/router.ts lines 93-105
for (const handler of routes) {
const response = await handler(req, runtime, url, pathname);
if (response !== null) {
return response; // First match wins
}
}
This "first hit wins" pattern ensures predictable routing behavior where explicit handlers override generic ones.
Fallback 404 Handling
If the loop completes without any handler returning a Response, handleServerRequest falls back to a generic JSON 404 response: { error: 'Not found' }.
Namespace-Absorbing Handlers and Built-in Routes
Instatic employs namespace-absorbing handlers that claim entire URL prefixes and handle their own 404 logic internally, preventing accidental fall-through to generic handlers.
Admin UI and Static Asset Delivery
The tryServeAdminApp handler (lines 14-30) manages the /admin namespace, serving the built admin SPA or redirecting to the Vite dev server in development mode. Similarly, tryServeSiteCssNamespace (lines 80-83) absorbs all requests under /_instatic/css/, returning its own 404 responses for unknown assets within that prefix.
Public Page Resolution
The final handler in the array, tryServePublicRoute, delegates to renderPublicResolution in server/publish/publicRouter.ts. This catch-all handler resolves visitor-facing URLs through a multi-layer pipeline involving fast-path resolution, caching, and dynamic fragment rendering.
Adding Custom Routes to Instatic
To extend Instatic with custom endpoints, implement a RouteHandler and insert it into the ordered routes array:
// server/router.ts
import { mySpecialHandler } from './handlers/mySpecial'
const routes: readonly RouteHandler[] = [
// ...existing handlers...
mySpecialHandler, // ← new endpoint
tryServePublicRoute, // keep the public page resolver last
]
Then implement the handler logic:
// server/handlers/mySpecial.ts
export async function mySpecialHandler(
req: Request,
_runtime: ServerRuntime,
_url: URL,
pathname: string,
): Promise<Response | null> {
if (req.method !== 'GET' || pathname !== '/special') return null
return new Response(JSON.stringify({ message: 'Hello from /special' }), {
headers: { 'content-type': 'application/json' },
})
}
Because mySpecialHandler appears before the generic public route, requests to /special are handled by your custom logic and never reach the page-rendering pipeline.
Key Files in the Routing Stack
server/router.ts– Core dispatcher,RouteHandlercontract, and built-in handler orchestrationserver/publish/publicRouter.ts– Visitor-facing URL resolution with Layer A fast-path, Layer B cache, and Layer C dynamic renderingserver/handlers/cms.ts– Admin CMS namespace API (/admin/api/cms/)server/handlers/cms/loop.ts,server/handlers/cms/hole.ts,server/handlers/cms/moduleJs.ts– Implementations for_instatic/loop,_instatic/hole, and_instatic/module-jsnamespacesserver/static.ts– Static asset delivery helpers and admin SPA serving utilitiesserver/publish/siteCssBundle.ts– CSS bundle serving for the/_instatic/css/namespace
Summary
- RouteHandler Contract: Instatic routing relies on async functions returning
Response | null, defined inserver/router.ts. - Ordered Execution: The
routesarray processes handlers sequentially; the first non-null response terminates the loop. - Namespace Isolation: Prefix-absorbing handlers like
tryServeSiteCssNamespaceprevent fall-through to generic routes. - Extensibility: Custom endpoints are added by implementing the
RouteHandlertype and inserting the function into the immutable routes table. - Bun Runtime: The entire router runs on Bun without external routing dependencies.
Frequently Asked Questions
What runtime does Instatic use for server-side routing?
Instatic runs its server-side routing exclusively on Bun, utilizing the runtime's native HTTP server capabilities without relying on Express, Fastify, or other external routing frameworks.
How does Instatic determine which handler processes a request?
The router iterates through the routes array in declared order, awaiting each RouteHandler until one returns a non-null Response. This "first match wins" pattern ensures that specific handlers override generic ones.
Can I add custom API endpoints to Instatic?
Yes. Create a function conforming to the RouteHandler type that returns a Response for your specific pathname and method, or null otherwise. Import this function into server/router.ts and insert it into the routes array before the catch-all tryServePublicRoute handler.
What happens if no route matches the incoming request?
If no handler in the routes array returns a Response, handleServerRequest returns a generic JSON 404 response with the body { error: 'Not found' } and appropriate HTTP status code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →