Routing#
Buntok provides a fast, type-safe routing system built on a trie-based data structure with AOT compilation. Routes are matched in O(1) for static paths and via efficient trie traversal for dynamic routes.
Basic Routes#
Register handlers for any HTTP method using the corresponding app method:
import { Buntok } from "@buntok/core";
const app = new Buntok();
// Classic style
app.get("/", (ctx) => {
return ctx.json({ message: "Hello, World!" });
});
// Elysia-style flexible return (also supported)
app.get("/hello", () => "hello world"); // text/plain
app.get("/json", () => ({ hello: "buntok" })); // application/json
app.get("/users/:id", ({ params }) => params); // { id: "123" }
// Static value handlers - response baked at registration, served natively
app.get("/ping", "pong"); // text/plain
app.get("/version", new Response("v2", { status: 200 })); // Response passthrough
app.post("/users", async (ctx) => {
const body = await ctx.body();
return ctx.json({ created: true, ...body }, 201);
});
app.put("/users/:id", (ctx) => {
return ctx.json({ updated: true });
});
app.delete("/users/:id", (ctx) => {
return ctx.json({ deleted: true });
});
app.listen(1212);A handler may also be a raw value (string or Response) instead of a function. The response is recorded at registration and served natively from Bun.serve routes - Bun answers the request without running JS per request. Promotion to native routes is skipped (the normal JS path handles it) when global or route middleware, request logging, dynamic paths, WebSocket paths, or non-standard methods are present.
HTTP Methods#
Buntok supports all standard HTTP methods plus the Bun-specific QUERY method:
| Method | Handler | Description |
|---|---|---|
| GET | app.get() | Retrieve resources |
| POST | app.post() | Create resources |
| PUT | app.put() | Replace resources |
| PATCH | app.patch() | Partial update |
| DELETE | app.delete() | Remove resources |
| OPTIONS | app.options() | CORS preflight |
| HEAD | app.head() | Headers only (no body) |
| ALL | app.all() | Matches all methods above |
| QUERY | app.query() | Bun-specific: GET with body |
app.all() registers a handler for all 7 standard HTTP methods (GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS). It does not include QUERY.HEAD request is answered by the GET route (body stripped, 200) when no explicit HEAD route is registered — same as Elysia, Hono, and Express. Paths without a GET route still return 404.app.get("/users/admin") takes precedence over app.get("/users/:id") regardless of registration order. If you register the same static method and path more than once, the later registration replaces the earlier handler.Multiple Methods#
Use app.all() to handle multiple methods with a single handler:
app.all("/health", (ctx) => {
return ctx.json({ status: "ok" });
});Route Parameters#
Capture dynamic segments from the URL using the :param syntax. Parameters are extracted from the path and available on ctx.params.
// Single parameter - classic
app.get("/users/:id", (ctx) => {
const { id } = ctx.params;
return ctx.json({ userId: id });
});
// Single parameter - Elysia-style
app.get("/users/:id", ({ params }) => params); // { id: "123" }
app.get("/users/:id", ({ params: { id } }) => id); // "123" text/plain
// Multiple parameters
app.get("/posts/:postId/comments/:commentId", (ctx) => {
const { postId, commentId } = ctx.params;
return ctx.json({ postId, commentId });
});
app.get("/posts/:postId/comments/:commentId", ({ params }) => params);string. Use Number() or parseInt() to convert to a number.Type-Safe Parameters#
Buntok uses TypeScript template literal types to infer route parameters at compile time. When you define a route like "/users/:id", the type of ctx.params is automatically { id: string }.
app.get("/users/:id", (ctx) => {
// ctx.params is typed as { id: string }
const { id } = ctx.params; // ✅ Type-safe
const { name } = ctx.params; // ❌ Type error: 'name' does not exist
});Wildcard Routes#
Use * to match any path after a prefix. The matched value is accessible via ctx.params["*"].
app.get("/files/*", (ctx) => {
const path = ctx.params["*"];
return ctx.json({ filePath: path });
});
app.get("/files/*", ({ params }) => params["*"]); // shorthand
// GET /files/documents/report.pdf
// Response: { "filePath": "documents/report.pdf" }Query Parameters#
Query parameters are parsed lazily from the URL and available on ctx.query. They are cached after first access.
app.get("/search", (ctx) => {
const { q, page, limit } = ctx.query;
return ctx.json({ query: q, page, limit });
});
app.get("/search", ({ query }) => query); // shorthand
// GET /search?q=buntok&page=1&limit=10
// Response: { "query": "buntok", "page": "1", "limit": "10" }string. For type-safe validation, use zValidator("query", schema).Route Groups#
Group related routes under a common prefix using app.group(). Groups support nested grouping and shared middleware.
const api = app.group("/api");
api.get("/users", listUsers);
api.post("/users", createUser);
api.get("/users/:id", getUser);
// Registered routes:
// GET /api/users
// POST /api/users
// GET /api/users/:idNested Groups#
Groups can be nested. Child groups inherit the parent's prefix and middleware.
const api = app.group("/api");
const v1 = api.group("/v1");
const v2 = api.group("/v2");
v1.get("/users", listUsersV1);
v2.get("/users", listUsersV2);
// Registered routes:
// GET /api/v1/users
// GET /api/v2/usersGroup Middleware#
Apply middleware to all routes in a group using group.use(). Group middleware runs after global middleware but before route-level middleware.
const api = app.group("/api");
api.use(cors);
api.use(rateLimit);
api.get("/users", listUsers);
api.post("/users", createUser);
// Execution order for GET /api/users:
// cors → rateLimit → listUsersMiddleware#
Middleware functions execute before the final handler. They receive the context and a next function to pass control forward.
Route-Level Middleware#
const auth = async (ctx, next) => {
const token = ctx.getCookie("token");
if (!token) return ctx.json({ error: "Unauthorized" }, 401);
// verify token...
return next();
};
app.get("/admin", auth, (ctx) => {
return ctx.json({ secret: "data" });
});
// Multiple middleware
app.get("/admin/settings", auth, checkRole("admin"), (ctx) => {
return ctx.json({ settings: {} });
});Global Middleware#
Use app.use() to register middleware that runs for every request, regardless of route.
// Runs for every request
app.use(logger);
app.cors({ origin: "*" }); // Use app.cors() for CORS
// Execution order:
// logger → cors → route-level middleware → handlerExecution Order#
Middleware executes in this order:
- Global middleware (
app.use()) - Group middleware (
group.use()) - Route-level middleware (inline or
@Use()decorator) - Final handler
Static File Serving#
Serve static files from a directory using app.static().
// Serve files from ./public at /public/*
app.static("/public", "./public");
// Serve files from ./assets at /files/*
app.static("/files", "./assets");Security#
Directory traversal attacks (e.g., /files/../../etc/passwd) are automatically blocked. Requests that escape the base directory return 403 Forbidden.
Caching#
Static file responses include Cache-Control: public, max-age=3600 (1 hour). ETag support is also included - if the client sends If-None-Match and the ETag matches, the server returns 304 Not Modified.
WebSocket#
Register WebSocket endpoints using app.ws(). WebSocket paths must be static (no parameters or wildcards).
app.ws("/chat", {
open: (ws) => {
ws.send(JSON.stringify({ type: "connected" }));
},
message: (ws, message) => {
// Broadcast to all connected clients
ws.publish("chat", message);
},
close: (ws, code, reason) => {
console.log("Client disconnected:", code);
},
});ws.data.ctx provides access to the request context, cookies, and DI container.Error Handling#
Override the global error and 404 handlers using app.onError() and app.notFound().
// Custom error handler
app.onError((err, ctx) => {
console.error(err);
return ctx.json(
{ success: false, error: err.message },
500
);
});
// Custom 404 handler
app.notFound((ctx) => {
return ctx.json(
{ success: false, error: "Not Found", path: ctx.request.url },
404
);
});Built-in Error Classes#
Throw HTTP errors to trigger the error handler with the appropriate status code:
| Error Class | Status |
|---|---|
| BadRequestError | 400 |
| UnauthorizedError | 401 |
| ForbiddenError | 403 |
| NotFoundError | 404 |
| MethodNotAllowedError | 405 |
| ConflictError | 409 |
| UnprocessableEntityError | 422 |
| TooManyRequestsError | 429 |
| InternalServerError | 500 |
| ServiceUnavailableError | 503 |
import { NotFoundError, ForbiddenError } from "@buntok/core";
app.get("/users/:id", async (ctx) => {
const user = await findUser(ctx.params.id);
if (!user) throw new NotFoundError("User not found");
if (!canView(user, ctx.user)) throw new ForbiddenError();
return ctx.json(user);
});Performance#
Buntok's router is built for speed:
- Static routes - Stored in a flat
Mapfor O(1) lookup - Dynamic routes - Trie-based with efficient JS implementation (optimized over native FFI)
- LRU cache - 2048-entry cache avoids re-traversal for repeated paths
- AOT compilation - Middleware pipelines compiled into single functions at boot
API Reference#
| Method | Description |
|---|---|
| app.get(path, ...handlers) | Register GET route |
| app.post(path, ...handlers) | Register POST route |
| app.put(path, ...handlers) | Register PUT route |
| app.patch(path, ...handlers) | Register PATCH route |
| app.delete(path, ...handlers) | Register DELETE route |
| app.options(path, ...handlers) | Register OPTIONS route |
| app.head(path, ...handlers) | Register HEAD route |
| app.all(path, ...handlers) | Register all HTTP methods |
| app.query(path, ...handlers) | Register QUERY (Bun-specific) |
| app.group(prefix) | Create a route group |
| app.static(routePath, dir) | Serve static files |
| app.ws(path, handler) | Register WebSocket endpoint |
| app.use(middleware) | Register global middleware |
| app.onError(handler) | Override error handler |
| app.notFound(handler) | Override 404 handler |
Next Steps#
- Controllers - Organize routes with class-based controllers
- Middleware - Learn more about middleware patterns
- Validation - Validate request data with Zod
- Context - Explore the full Context API