Buntok API#
The Buntok class is the core of Buntok. It provides HTTP routing, middleware, dependency injection, and server lifecycle management.
Constructor#
const app = new Buntok();HTTP Route Methods#
All route methods accept a path, optional middlewares, and a handler. They return this for fluent chaining.
app.get("/users", handler)
.post("/users", handler)
.put("/users/:id", handler)
.patch("/users/:id", handler)
.delete("/users/:id", handler)
.options("/users", handler)
.head("/users", handler)
.all("/users", handler)
.query("/search", handler); // QUERY method (Bun-specific, supports body)| Method | HTTP Method | Notes |
|---|---|---|
get() | GET | |
post() | POST | |
put() | PUT | |
patch() | PATCH | |
delete() | DELETE | |
options() | OPTIONS | |
head() | HEAD | |
all() | All standard methods | GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS |
query() | QUERY | Bun-specific, supports request body |
group()#
Create a route group with a shared prefix. The group supports all HTTP methods, middleware, WebSocket, static files, and controller registration.
const api = app.group("/api/v1");
// Group-level middleware
api.use(cors);
api.use(auth);
// Routes in the group
api.get("/users", handler);
api.post("/users", handler);
// Nested groups
const admin = api.group("/admin");
admin.get("/dashboard", handler);Middleware & Error Handling#
| Method | Description |
|---|---|
use(middleware) | Add global middleware (runs on every request) |
onError(handler) | Set custom global error handler |
notFound(handler) | Set custom 404 handler |
cors()#
Configure CORS. Unlike app.use(cors(...)), this also ensures CORS headers are applied to error responses (4xx/5xx).
app.cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"],
headers: ["Content-Type", "Authorization"],
});Dependency Injection#
| Method | Description |
|---|---|
set(key, value) | Set a value in the DI map (accessible via ctx.di[key]) |
decorate(key, value) / decorate(record) | App-level value on ctx.di with a narrowed type - returns Buntok<DI & T> (app.set is the runtime equivalent without narrowing) |
setContainer(container) | Attach an IoC Container for automatic controller resolution |
getContainer() | Get the attached Container (creates one if none set) |
// Simple DI
app.set("db", database);
app.set("config", config);
// IoC Container
const container = new Container();
container.scan([UserController]);
app.setContainer(container);registerController()#
Register controllers decorated with @Controller. Accepts a single class, an instance, or an array.
// Single controller
app.registerController(UserController);
// Array of controllers
app.registerController([UserController, PostController]);
// On a group
const api = app.group("/api/v1");
api.registerController([UserController, PostController]);setContainer(), controllers with @Dependencies are resolved automatically.WebSocket#
| Method | Description |
|---|---|
ws(path, handler) | Register a WebSocket endpoint |
wsOptions(opts) | Configure Bun WebSocket options |
static()#
Serve static files with directory-traversal protection and ETag caching. Pass { native: true } to serve via Bun's built-in static router instead (no Cache-Control, empty-body 404 — see Static Files).
app.static("/public", "./public", {
maxAge: 3600, // 1 hour
cacheControl: "public, max-age=3600",
});apiDocs()#
Enable interactive Swagger UI documentation.
app.apiDocs({
path: "/docs", // default: "/docs"
title: "My API",
version: "1.0.0",
description: "API documentation",
safeOnProduction: true, // hide docs when NODE_ENV=production
});validateEnv()#
Validate process.env against a Zod schema. Exits with a clear error on failure.
import { Buntok } from "@buntok/core";
import { z } from "@buntok/core/middlewares/validator";
// Static method
const env = Buntok.validateEnv({
PORT: z.coerce.number().default(1212),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
// Instance method (backward compatible)
const env = app.validateEnv({ ... });model()#
Register validation schemas in a global registry, then reference them by name in zValidator, zResponse, validate() and friends - define once, reuse everywhere.
app.model("UserBody", z.object({ name: z.string().min(1) }));
// or register several at once
app.model({ UserIdParams: z.object({ id: z.string().uuid() }) });
app.post("/users", zValidator("body", "UserBody"), (ctx) =>
ctx.json(ctx.valid("body"), 201)
);
const schema = app.getModel("UserBody"); // throws if not registeredUnknown model "X". Register it with app.model("X", schema) before routes use it.listen()#
Start the HTTP server. Performs AOT compilation, auto-increments port on EADDRINUSE, and sets up graceful shutdown.
app.listen(1212, () => {
console.log("Server running on port 1212");
});
// Uses process.env.PORT or 1212 by default
app.listen();Lifecycle Hooks#
Global per-request hooks. Order per request: onRequest → use() → derive → onBeforeHandle → handler → onAfterHandle → middleware tail. Hooks registered after listen() take effect immediately (the AOT pipeline is recompiled).
app.onRequest((ctx) => {
if (ctx.request.headers.get("x-key") !== "s3cret")
return new Response("forbidden", { status: 403 }); // skips middleware + handler
});
app.derive(async () => ({ tenant: await resolveTenant() })); // → ctx.store.tenant
app.onBeforeHandle((ctx) => {
if (!ctx.store.tenant) return new Response("no tenant", { status: 400 }); // skips handler
});
app.onAfterHandle((_ctx, res) => {
res.headers.set("x-powered-by", "benchmark"); // or return a new Response to replace
});
app.onStart(() => console.log("ready")); // fires when listen() succeeds
app.onStop(() => console.log("bye")); // fires during close()Returning a Response from onRequest / onBeforeHandle short-circuits everything after it; returning one from onAfterHandle replaces the response. derive merges into ctx.store. Registering any pipeline hook disables native static-route promotion - all requests flow through the composed pipeline.
Testing & Serverless#
| Method | Description |
|---|---|
request(input, init?) | Send a test request without binding to a port (fast) |
fetch(request) | Standard fetch handler for serverless platforms (Vercel, Cloudflare) |
// In tests
const response = await app.request("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John" }),
});
// Serverless (Vercel)
export default { fetch: (req) => app.fetch(req) };Configuration#
| Method | Description |
|---|---|
disable("x-powered-by") | Disable the X-Powered-By header |
enable("x-powered-by") | Re-enable the X-Powered-By header |
enableReusePort(bool?) | Enable SO_REUSEPORT (Linux only) |
icon(path) | Set custom favicon path (default: ./public/favicon.ico) |
Properties#
| Property | Type | Description |
|---|---|---|
server | Server | undefined | Bun Server instance (available after listen()) |
di | DI | Dependency injection map |
Graceful Shutdown#
Production-ready shutdown with resource cleanup, in-flight request drain, and signal handling.
const app = new Buntok({
handleSignals: true, // auto-register SIGINT/SIGTERM (default: true)
shutdownTimeout: 30_000, // max ms to wait for resource cleanup
});
// Register resources for cleanup on shutdown
app.registerResource({
name: "db-pool",
close: async () => {
await db.$disconnect();
},
});
// Manual shutdown (e.g., in tests)
await app.close({ timeout: 5_000 }); // stop accepting, flush in 5s
await app.shutdown(); // alias for app.close()| Option / Method | Description |
|---|---|
handleSignals | Auto-register SIGINT/SIGTERM handlers (default: true) |
shutdownTimeout | Max ms to wait for resource cleanup (default: 30000) |
app.close(options?) | Stop accepting traffic, flush resources - { timeout?, force? } |
app.registerResource(resource) | Register a resource for cleanup - { name?, close?(), dispose?() } |
WebSocket Configuration#
Configure Bun WebSocket options globally (permessage-deflate, payload limits, backpressure).
app.wsOptions({
perMessageDeflate: true, // enable compression (default: false)
maxPayloadLength: 1024 * 1024, // 1MB max message size
idleTimeout: 30, // seconds before idle connection closes
maxBackpressure: 1024, // max bytes buffered per connection
publishToSelf: false, // don't deliver to sender's own connection
});Trusted Proxy#
Configure which proxy peers supply X-Forwarded-For, X-Forwarded-Proto, etc. headers. Not trusted by default.
// Trust only specific proxy addresses
app.setTrustedProxy({
addresses: ["127.0.0.1", "10.0.0.0/8"],
depth: 1, // how many proxies to trust (default: 1)
});
// Trust all (e.g., behind reverse proxy in same network)
app.setTrustedProxy();Full Example#
import { Buntok } from "@buntok/core";
import { z } from "@buntok/core/middlewares/validator";
const env = Buntok.validateEnv({
PORT: z.coerce.number().default(1212),
});
const app = new Buntok();
// Global middleware
app.use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
return next();
});
// CORS
app.cors({ origin: "*" });
// API docs
app.apiDocs({ title: "My API", version: "1.0.0" });
// Routes
app.get("/", (ctx) => ctx.json({ hello: "world" }));
app.get("/users/:id", (ctx) => ctx.json({ id: ctx.params.id }));
// Error handling
app.onError((err, ctx) => {
console.error(err);
return ctx.json({ error: "Internal Server Error" }, 500);
});
// Start server
app.listen(env.PORT);