# How Instatic Handles Server-Side Routing: Ordered Handler Patterns in Bun

> Discover how Instatic uses ordered handler patterns for efficient server-side routing in Bun. Learn about its custom Bun router for fast request processing.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-02

---

**Instatic implements server-side routing through a lightweight, hand-rolled router in [`server/router.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```typescript
// 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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```typescript
// 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:

```typescript
// 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`](https://github.com/CoreBunch/Instatic/blob/main/server/router.ts)** – Core dispatcher, `RouteHandler` contract, and built-in handler orchestration
- **[`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts)** – Visitor-facing URL resolution with Layer A fast-path, Layer B cache, and Layer C dynamic rendering
- **[`server/handlers/cms.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms.ts)** – Admin CMS namespace API (`/admin/api/cms/`)
- **[`server/handlers/cms/loop.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/loop.ts)**, **[`server/handlers/cms/hole.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/hole.ts)**, **[`server/handlers/cms/moduleJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/moduleJs.ts)** – Implementations for `_instatic/loop`, `_instatic/hole`, and `_instatic/module-js` namespaces
- **[`server/static.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/static.ts)** – Static asset delivery helpers and admin SPA serving utilities
- **[`server/publish/siteCssBundle.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/siteCssBundle.ts)** – CSS bundle serving for the `/_instatic/css/` namespace

## Summary

- **RouteHandler Contract**: Instatic routing relies on async functions returning `Response | null`, defined in [`server/router.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/router.ts).
- **Ordered Execution**: The `routes` array processes handlers sequentially; the first non-null response terminates the loop.
- **Namespace Isolation**: Prefix-absorbing handlers like `tryServeSiteCssNamespace` prevent fall-through to generic routes.
- **Extensibility**: Custom endpoints are added by implementing the `RouteHandler` type 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`](https://github.com/CoreBunch/Instatic/blob/main/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.