Buntok API#

The Buntok class is the core of Buntok. It provides HTTP routing, middleware, dependency injection, and server lifecycle management.

Constructor#

typescript
const app = new Buntok();

HTTP Route Methods#

All route methods accept a path, optional middlewares, and a handler. They return this for fluent chaining.

typescript
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)
MethodHTTP MethodNotes
get()GET
post()POST
put()PUT
patch()PATCH
delete()DELETE
options()OPTIONS
head()HEAD
all()All standard methodsGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
query()QUERYBun-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.

typescript
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#

MethodDescription
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).

typescript
app.cors({
  origin: "*",
  methods: ["GET", "POST", "PUT", "DELETE"],
  headers: ["Content-Type", "Authorization"],
});

Dependency Injection#

MethodDescription
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)
typescript
// 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.

typescript
// 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]);
Info
When a Container is set via setContainer(), controllers with @Dependencies are resolved automatically.

WebSocket#

MethodDescription
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).

typescript
app.static("/public", "./public", {
  maxAge: 3600,           // 1 hour
  cacheControl: "public, max-age=3600",
});

apiDocs()#

Enable interactive Swagger UI documentation.

typescript
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.

typescript
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.

typescript
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 registered
Info
Names are module-global - use unique names in tests. Unknown names throw at route registration: Unknown 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.

typescript
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).

typescript
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#

MethodDescription
request(input, init?)Send a test request without binding to a port (fast)
fetch(request)Standard fetch handler for serverless platforms (Vercel, Cloudflare)
typescript
// 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#

MethodDescription
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#

PropertyTypeDescription
serverServer | undefinedBun Server instance (available after listen())
diDIDependency injection map

Graceful Shutdown#

Production-ready shutdown with resource cleanup, in-flight request drain, and signal handling.

typescript
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 / MethodDescription
handleSignalsAuto-register SIGINT/SIGTERM handlers (default: true)
shutdownTimeoutMax 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).

typescript
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.

typescript
// 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#

typescript
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);