buntok
v0.2.3
Published
High performance, batteries-included Bun backend framework
Maintainers
Readme
Features
| Feature | Description |
|---------|-------------|
| ⚡ Lightning Fast | Built on Bun runtime for maximum performance |
| 🔌 Middleware Support | Global, per-route, and group-level middleware with chaining |
| 🛣️ Dynamic Routing | Route parameters (:id), wildcards (*), and route groups |
| 📁 Static File Serving | Serve files from any directory with automatic MIME detection |
| 📝 Built-in Logger | Colored output, JSON mode, and file rotation |
| 🎨 Custom Favicon | Easy favicon configuration with fallback |
| 🔒 TypeScript First | Full type safety out of the box |
| ✅ Request Validation | Validate body and params with any schema library |
| 🌐 CORS Built-in | Cross-origin support with automatic headers |
| 💉 Type-Safe DI | Dependency injection with full autocomplete support |
| 📡 SSE Support | Built-in Server-Sent Events helper for real-time data |
| 🚦 Rate Limiting | In-memory rate limiter with sliding window support |
| 🆔 Request ID | Automatic unique request ID generation |
| ⏱️ Response Time | Automatic response time measurement |
| 🏥 Health Check | Built-in health check endpoint helper |
| 🍪 Cookie Helpers | Parse, set, and delete cookies easily |
| 📦 Code Generation | CLI tool for generating schema, repo, service, controller |
| 🧠 Cache System | Built-in driver-based Cache manager |
| 🔄 Queue System | Built-in driver-based background Queues |
| ⏱️ Task Scheduler | Native CRON job support via @CronJob |
| 🛡️ Auth Guards | Granular access control via @UseGuard |
| 📁 File Uploads | Configurable multipart parser with storage drivers |
| 🤖 AI Ready | Native Vercel AI SDK Support (Data Stream Protocol) & Semantic Caching |
Requirements
- Bun v1.0 or higher
- TypeScript 5.0 or higher (optional, recommended)
Installation
Quick Start with CLI (Recommended)
The fastest way to start a new Buntok project:
bunx create-buntok my-app
cd my-app
bun run devThis will:
- Create a new directory with project scaffolding
- Install all dependencies
- Set up TypeScript, Biome, and VS Code config
- Include a default favicon
Manual Installation
If you prefer to set up manually:
# Create project directory
mkdir my-app && cd my-app
# Initialize package.json
bun init
# Install Buntok
bun add buntok
# Install dev dependencies
bun add -D @biomejs/biome @types/bun typescriptQuick Start
Minimal Example
// src/index.ts
import { App } from "buntok";
const app = new App();
app.get("/", (ctx) => {
return ctx.json({ message: "Hello from Buntok!" });
});
// Server auto-starts on port 1212 (or PORT env var)That's it! No app.listen() required. Buntok auto-starts the server after all routes are registered.
Step-by-Step Breakdown
// 1. Import the App class
import { App } from "buntok";
// 2. Create application instance
const app = new App();
// 3. Register routes
app.get("/", (ctx) => {
// ctx = Context object (request, params, helpers)
return ctx.json({ message: "Hello!" });
});
// 4. Optional: Explicitly start server
app.listen(3000);Run Your Server
# Development (with hot reload)
bun run dev
# Production
bun run startCore Concepts
App
The App class is the heart of Buntok. It manages:
- Route registration
- Middleware pipeline
- Server lifecycle
- Dependency injection
import { App } from "buntok";
// Without DI
const app = new App();
// With DI (type-safe)
type Container = { db: Database; config: Config };
const app = new App<Container>();Router
Buntok uses a custom radix tree router for fast route matching:
- Static routes:
/users— exact match - Dynamic routes:
/users/:id— captures URL segments - Wildcard routes:
/files/*— catches everything after
Route matching order:
- Static routes (fastest)
- Dynamic routes
- Wildcard routes (slowest)
Context
The Context object is passed to every handler and middleware:
app.get("/example", (ctx) => {
ctx.request // Raw Request object
ctx.params // Route parameters
ctx.store // Shared data between middleware
ctx.di // Dependencies (DI)
});Strongly-Typed Context for Decorators (ZodCtx)
When using class controllers and decorators, TypeScript cannot automatically infer ctx.params or ctx.valid() types due to language limitations on Method Decorators. To fix this DX issue, Buntok provides a flexible ZodCtx helper:
import { Controller, Get, Post, ZodCtx, Use, zValidator } from "buntok";
import { z } from "zod";
const createUserSchema = z.object({ name: z.string(), age: z.number() });
const paramSchema = z.object({ id: z.string() });
const paginationSchema = z.object({ page: z.number(), limit: z.number() });
@Controller("/users")
export class UserController {
// 1. Path Parameters Inference
@Get("/:id/posts/:postId")
async getPost(ctx: ZodCtx<{}, "/:id/posts/:postId">) {
// TypeScript knows these exist and are strings!
const { id, postId } = ctx.params;
}
// 2. Body Inference from zValidator
@Post("/")
@Use(zValidator("body", createUserSchema))
async create(ctx: ZodCtx<{ body: typeof createUserSchema }>) {
// ctx.valid("body") automatically infers { name: string, age: number }
const body = ctx.valid("body");
}
// 3. Combined Validation (Query & Params)
@Get("/:id/search")
@Use(zValidator("params", paramSchema))
@Use(zValidator("query", paginationSchema))
async search(ctx: ZodCtx<{ params: typeof paramSchema; query: typeof paginationSchema }>) {
const p = ctx.valid("params"); // { id: string }
const q = ctx.valid("query"); // { page: number, limit: number }
}
}Validation (zValidator)
Buntok provides a built-in zValidator middleware to cleanly validate incoming requests against Zod schemas.
import { zValidator } from "buntok";
import { z } from "zod";
// Validating JSON Body (Default)
app.post("/users", zValidator("body", z.object({ name: z.string() })), (ctx) => {
const data = ctx.valid("body"); // Type inferred safely!
return ctx.json(data);
});Content-Type Support & File Uploads
By default, zValidator expects application/json. You can validate different content types by passing a third argument.
With Zod v4, you can also natively validate File objects directly in multipart/form-data!
const uploadSchema = z.object({
title: z.string(),
file: z.file().max(5 * 1024 * 1024) // 5MB max
});
app.post("/upload", zValidator("body", uploadSchema, { contentType: "multipart/form-data" }), (ctx) => {
const { title, file } = ctx.valid("body");
return ctx.json({
message: "Valid upload!",
title: title,
fileName: file.name
});
});Note: Always use ctx.valid("body") to read data parsed by zValidator. Using await ctx.body() will throw a Body already used error because the request stream is already consumed by the validator.
> **Note:** If you use the functional chaining API (e.g., `app.get("/:id", (ctx) => {})`), path parameters are inferred 100% automatically without needing `RouteContext`!
### Middleware
Middleware functions process requests before/after handlers:
```typescript
// Middleware signature
type Middleware = (ctx: Context, next: () => Promise<Response>) => Response | Promise<Response>;Execution order:
Request → Global Middleware → Route Middleware → Handler → ResponseAPI Reference
new App<DI>()
Creates a new application instance.
// Without DI
const app = new App();
// With DI (type-safe dependency injection)
type Container = {
db: Database;
config: { port: number };
};
const app = new App<Container>();Behavior:
- Initializes router
- Registers default favicon route
- Sets up auto-start (server starts when event loop is idle)
HTTP Methods
app.get(path, ...handlers)
Register a GET route handler.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| path | string | Yes | Route path pattern |
| ...handlers | Middleware[] \| Handler | Yes | Middleware(s) + final handler |
Path Patterns:
| Pattern | Example URL | Captures |
|---------|-------------|----------|
| /users | /users | None |
| /users/:id | /users/123 | { id: "123" } |
| /files/* | /files/docs/readme.md | { "*": "docs/readme.md" } |
Returns: this (enables method chaining)
Examples:
// Simple route
app.get("/", (ctx) => {
return ctx.text("Hello, World!");
});
// With route parameter
app.get("/users/:id", (ctx) => {
const userId = ctx.params.id; // "123"
return ctx.json({ id: userId });
});
// With middleware
app.get("/admin", authMiddleware, (ctx) => {
return ctx.json({ message: "Welcome, admin!" });
});
// Chaining
app
.get("/a", handler1)
.get("/b", handler2)
.get("/c", handler3);app.post(path, ...handlers)
Register a POST route handler. Typically used for creating resources.
Parameters: Same as app.get()
Example:
app.post("/users", async (ctx) => {
// Parse JSON body
const body = await ctx.body<{ name: string; email: string }>();
// Create user in database
const user = await db.users.create(body);
// Return with 201 Created status
return ctx.json({ data: user }, 201);
});app.put(path, ...handlers)
Register a PUT route handler. Typically used for full resource replacement.
Parameters: Same as app.get()
Example:
app.put("/users/:id", async (ctx) => {
const { id } = ctx.params;
const body = await ctx.body<{ name: string; email: string }>();
const updated = await db.users.update(id, body);
return ctx.json({ data: updated });
});app.delete(path, ...handlers)
Register a DELETE route handler. Typically used for removing resources.
Parameters: Same as app.get()
Example:
app.delete("/users/:id", async (ctx) => {
const { id } = ctx.params;
await db.users.delete(id);
return ctx.status(204); // No Content
});app.options(path, ...handlers)
Register an OPTIONS route handler. Primarily used for CORS preflight requests.
Parameters: Same as app.get()
Example:
app.options("/api/*", (ctx) => {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
});Tip: Consider using the built-in cors() middleware instead of manual OPTIONS handling.
app.query(path, ...handlers)
Register a QUERY route handler. Like GET but with body support for complex queries.
Parameters: Same as app.get()
Key Features:
- ✅ Idempotent - Safe to retry (like GET)
- ✅ Body allowed - Send complex queries in body (like POST)
- ✅ Cacheable - Proxies/CDNs can cache responses
- ✅ Safe - Read-only, never modifies server state
When to use QUERY:
- Complex filters that exceed URL length limits
- Large query payloads (SQL, JSONPath, etc.)
- When you need idempotent reads with body
Example:
// Complex query with filters
app.query("/orders", async (ctx) => {
const filters = await ctx.body<{
select: string[];
where: Record<string, unknown>;
limit: number;
sort: { field: string; order: "asc" | "desc" }[];
}>();
// Build query from filters
const results = await db.query(filters);
return ctx.json({ data: results });
});
// Client sends:
// QUERY /orders
// Content-Type: application/json
//
// {
// "select": ["id", "customer", "total"],
// "where": { "status": "active", "total": { "gt": 100 } },
// "limit": 10,
// "sort": [{ "field": "created_at", "order": "desc" }]
// }Comparison:
| Feature | GET | QUERY | POST | |---------|-----|-------|------| | Body allowed | ❌ | ✅ | ✅ | | Idempotent | ✅ | ✅ | ❌ | | Cacheable | ✅ | ✅ | ⚠️ | | Safe (read-only) | ✅ | ✅ | ❌ |
Note: QUERY is a new HTTP method standardized in June 2026 (RFC 10008). Support is growing but may not be available in all clients yet.
app.ws(path, handler)
Register a WebSocket endpoint. Backed directly by Bun's native WebSocket server for maximum performance.
Note on Frontend Clients:
Buntok uses Raw WebSockets (RFC 6455) natively.
⚠️ Do NOT use socket.io-client to connect to Buntok, as Socket.IO uses a proprietary Engine.IO protocol that is incompatible with Raw WebSockets.
✅ Recommended clients for React/Next.js: react-use-websocket, partysocket, or native WebSocket.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| path | string | Yes | Exact route path for the WebSocket endpoint |
| handler | WSHandler | Yes | WebSocket event handlers (open, message, close, drain) |
Example:
app.ws("/ws", {
open: (ws) => {
// Treat the user's ID as their own private "room"
const userId = "user-123"; // Retrieve this from token/auth
ws.subscribe(`user_${userId}`);
console.log("Client connected!");
},
message: (ws, msg) => {
// For handling incoming client events, parse the JSON
const payload = JSON.parse(String(msg));
if (payload.event === "typing") {
// Do something
}
}
});Broadcasting Events to Specific Users (From API/Jobs):
You can access the Bun Server instance natively via app.server to publish messages from anywhere in your backend! Since Buntok uses raw WebSockets, the standard way to send "events" is to wrap them in a JSON payload.
app.post("/api/checkout", (ctx) => {
const userId = "user-123";
// Format standard event JSON
const payload = JSON.stringify({
event: "order_completed",
data: { orderId: 456, amount: 100 }
});
// Target ONLY this specific user
app.server?.publish(`user_${userId}`, payload);
return ctx.success("Notification sent!");
});Client-Side Example (Vanilla JS / React):
const ws = new WebSocket("ws://localhost:1212/ws");
ws.onmessage = (e) => {
const payload = JSON.parse(e.data);
if (payload.event === "order_completed") {
alert("Order Completed: " + payload.data.orderId);
}
};ctx.sse(callback, options?)
Start a Server-Sent Events (SSE) stream. SSE is a lightweight alternative to WebSockets when you only need one-way communication (Server to Client), such as real-time notifications or live scores.
Key Benefits of SSE:
- Built-in auto-reconnection in the browser.
- Works over standard HTTP/HTTPS (passes through corporate firewalls easily).
- Has native
.sendEvent()support out of the box.
Example: Streaming Data
app.get("/stream", (ctx) => {
return ctx.sse((stream) => {
// Send a named event
stream.sendEvent("connected", { status: "ok" });
const timer = setInterval(() => {
// Send data periodically
stream.sendEvent("price_update", { price: Math.random() });
}, 1000);
// Buntok will automatically call this when the client disconnects!
stream.onClose(() => {
clearInterval(timer);
console.log("Client disconnected");
});
});
});Client-Side Example:
const eventSource = new EventSource("/stream");
// Listen to specific events
eventSource.addEventListener("price_update", (e) => {
const data = JSON.parse(e.data);
console.log("New price:", data.price);
});app.registerController(ControllerClass)
Register all routes declared on a class decorated with @Controller. This instantiates the class once at boot time and wires each decorated method (@Get, @Post, etc.) automatically.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| ControllerClass | Class | Yes | The class containing decorated route methods |
Example:
import { Controller, Get } from "buntok";
@Controller("/users")
class UserController {
@Get("/")
getAll(ctx: Context) {
return ctx.json([{ id: 1, name: "Pito" }]);
}
}
app.registerController(UserController);app.disable(feature) / app.enable(feature)
Disable or enable built-in framework features.
Supported Features:
"x-powered-by": Controls whether theX-Powered-By: buntokheader is sent with HTTP responses.
Example:
// Disable the X-Powered-By header for security/stealth
app.disable("x-powered-by");
// Re-enable it
app.enable("x-powered-by");app.use(middleware)
Add global middleware that runs on every request, before route handlers.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| middleware | Middleware | Yes | Middleware function |
Returns: this (chainable)
Middleware Signature:
type Middleware = (
ctx: Context,
next: () => Promise<Response>
) => Response | Promise<Response>;Behavior:
- Middleware runs in registration order
- Must call
next()to continue to next middleware/handler - Can modify request/response
Examples:
// Request logging
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
const response = await next();
const ms = Date.now() - start;
console.log(`← ${response.status} (${ms}ms)`);
return response;
});
// Add custom headers
app.use(async (ctx, next) => {
const response = await next();
response.headers.set("X-Custom-Header", "value");
return response;
});Order Matters:
// ✅ Correct order
app.use(logger); // Runs first
app.use(auth); // Runs second
app.get("/api", handler); // Runs last
// ❌ Wrong order - auth won't apply to /api
app.get("/api", handler);
app.use(auth);app.static(routePath, directory)
Serve static files from a directory.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| routePath | string | Yes | URL prefix for static files |
| directory | string | Yes | Local directory path (relative to cwd) |
Returns: this (chainable)
Behavior:
- Automatically detects MIME types
- Returns 404 if file not found
- Trailing slashes are handled automatically
Examples:
// Serve ./public directory at /assets
app.static("/assets", "./public");
// /assets/style.css → ./public/style.css
// /assets/logo.png → ./public/logo.png
// Serve at root
app.static("/", "./public");
// /index.html → ./public/index.htmlMIME Types Supported:
.html→text/html.css→text/css.js→application/javascript.json→application/json.png,.jpg,.gif→image/*.pdf→application/pdf- And many more...
app.icon(path)
Set custom favicon path.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| path | string | Yes | Path to favicon file |
Returns: this (chainable)
Default: ./public/favicon.ico
Behavior:
- Serves favicon at
/favicon.ico - Falls back to built-in icon if file not found
- Supports
.ico,.png,.svg
Examples:
// Custom icon
app.icon("./assets/custom-favicon.ico");
// Use default (no call needed)
// Automatically serves ./public/favicon.icoapp.onError(handler)
Set custom global error handler for uncaught exceptions.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| handler | ErrorHandler | Yes | Error handler function |
Returns: this (chainable)
Handler Signature:
type ErrorHandler = (err: Error, ctx: Context) => Response | Promise<Response>;Default Behavior:
- Logs error with stack trace
- Returns 500 in development
- Returns generic message in production
Examples:
// Custom error handler
app.onError((err, ctx) => {
console.error("Error:", err);
return ctx.json({
success: false,
error: "Something went wrong",
message: process.env.NODE_ENV === "production"
? "Internal server error"
: err.message,
}, 500);
});
// Global error handler with logging
app.onError((err, ctx) => {
logger.error("Unhandled error", {
message: err.message,
stack: err.stack,
path: new URL(ctx.request.url).pathname,
});
return ctx.json({ error: "Internal Server Error" }, 500);
});app.notFound(handler)
Set custom handler for 404 Not Found responses.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| handler | NotFoundHandler | Yes | Not found handler function |
Returns: this (chainable)
Handler Signature:
type NotFoundHandler = (ctx: Context) => Response | Promise<Response>;Default Behavior:
- Returns JSON with error message and path
Examples:
// Custom 404 handler
app.notFound((ctx) => {
return ctx.json({
success: false,
error: "Not Found",
path: new URL(ctx.request.url).pathname,
}, 404);
});
// HTML 404 page
app.notFound((ctx) => {
return ctx.text(`
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
</body>
</html>
`, 404);
});
// Redirect to home page
app.notFound((ctx) => {
return new Response(null, {
status: 302,
headers: { Location: "/" },
});
});Dependency Injection (DI)
Buntok provides type-safe dependency injection via generics.
Setup:
import { App } from "buntok";
// 1. Define your container type
class Database {
getUsers() { return [{ id: 1, name: "Pito" }]; }
}
class UserService {
constructor(private repo: Database) {}
getActiveUsers() { return this.repo.getUsers(); }
}
type Container = {
db: Database;
userService: UserService;
config: { dbName: string; port: number };
};
// 2. Pass Container to App
const app = new App<Container>();
// 3. Register dependencies (type-checked!)
const db = new Database();
app.set("db", db);
app.set("userService", new UserService(db));
app.set("config", { dbName: "buntok_db", port: 1212 });
// 4. Access in routes via ctx.di
app.get("/users", (ctx) => {
const users = ctx.di.db.getUsers(); // Auto-complete works!
const { config } = ctx.di; // Destructuring works!
return ctx.json({ data: users });
});app.set(key, value)
Store a dependency in the application container.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| key | keyof DI | Yes | Dependency name (must match Container type) |
| value | DI[K] | Yes | Value to store (must match type) |
Returns: this (chainable)
Behavior:
- Type-safe: TypeScript rejects wrong key names or value types
- Values accessible via
ctx.diin handlers - Useful for database instances, services, configs, singletons
Examples:
type Container = {
db: Database;
config: { port: number };
};
const app = new App<Container>();
app.set("db", new Database()); // ✅ OK
app.set("config", { port: 3000 }); // ✅ OK
app.set("wrong", "value"); // ❌ TypeScript error!app.group(prefix)
Create a route group with shared prefix and middleware.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| prefix | string | Yes | URL prefix for all routes in group |
Returns: RouterGroup
RouterGroup Methods:
group.get(path, ...handlers)group.post(path, ...handlers)group.put(path, ...handlers)group.delete(path, ...handlers)group.use(middleware)— group-level middlewaregroup.group(prefix)— nested groups
Examples:
// Basic group
const api = app.group("/api");
api.get("/users", handler); // GET /api/users
api.post("/users", handler); // POST /api/users
api.get("/posts", handler); // GET /api/posts
// Nested groups
const v1 = api.group("/v1");
v1.get("/users", handler); // GET /api/v1/users
const v2 = api.group("/v2");
v2.get("/users", handler); // GET /api/v2/usersGroup Middleware:
const admin = app.group("/admin");
// Middleware applies to ALL routes in this group
admin.use(async (ctx, next) => {
const token = ctx.request.headers.get("Authorization");
if (!token) {
return ctx.json({ error: "Unauthorized" }, 401);
}
return next();
});
admin.get("/dashboard", handler); // Protected
admin.get("/settings", handler); // Protected
// Routes outside group are NOT affected
app.get("/public", publicHandler); // No auth requiredNested Group Middleware:
const api = app.group("/api");
// Parent middleware
api.use(loggerMiddleware);
const admin = api.group("/admin");
// Child middleware
admin.use(adminAuthMiddleware);
// GET /api/admin/dashboard runs:
// 1. loggerMiddleware
// 2. adminAuthMiddleware
// 3. handlerapp.listen(port?, callback?)
Start the HTTP server.
Parameters:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| port | number | No | 1212 | Port to listen on |
| callback | () => void | No | - | Called after server starts |
Returns: void
Port Resolution:
- Explicit
portparameter PORTenvironment variable- Default:
1212
Behavior:
- Server auto-starts if
listen()is never called - Calling
listen()multiple times is safe (no duplicate servers) - Displays BUNTOK banner on startup
Examples:
// Explicit port
app.listen(3000);
// Use environment variable
PORT=8080 bun run src/index.ts
// With callback
app.listen(3000, () => {
console.log("Server ready!");
});
// No call needed - auto-starts
const app = new App();
app.get("/", handler);
// Server starts on port 1212 automaticallyEnvironment Variables:
# Set custom port
PORT=8080 bun run src/index.ts
# Production
NODE_ENV=production PORT=80 bun run src/index.tsContext
The Context object provides access to the request and helper methods.
ctx.request
The raw Web Request object.
app.get("/headers", (ctx) => {
const userAgent = ctx.request.headers.get("User-Agent");
const contentType = ctx.request.headers.get("Content-Type");
const auth = ctx.request.headers.get("Authorization");
return ctx.json({ userAgent, contentType, hasAuth: !!auth });
});Common Properties:
ctx.request.method // "GET", "POST", etc.
ctx.request.url // "http://localhost:3000/users/123"
ctx.request.headers // Headers object
ctx.request.body // ReadableStream (use ctx.body() instead)ctx.params
Route parameters as key-value pairs. All values are strings.
// Route: /users/:id
app.get("/users/:id", (ctx) => {
const id = ctx.params.id; // "123" (string, not number!)
return ctx.json({ id });
});
// Route: /users/:userId/posts/:postId
app.get("/users/:userId/posts/:postId", (ctx) => {
const { userId, postId } = ctx.params;
return ctx.json({ userId, postId });
});
// Wildcard: /files/*
app.get("/files/*", (ctx) => {
const filePath = ctx.params["*"]; // "docs/readme.md"
return ctx.json({ path: filePath });
});Type Conversion:
app.get("/users/:id", (ctx) => {
// Params are always strings - convert manually
const id = Number(ctx.params.id);
if (Number.isNaN(id)) {
return ctx.json({ error: "Invalid ID" }, 400);
}
// Now id is a number
return ctx.json({ id });
});ctx.store
Shared data object between middleware and handlers. Useful for passing data down the pipeline.
// Set data in middleware
app.use(async (ctx, next) => {
ctx.store.user = await getUserFromRequest(ctx.request);
ctx.store.startTime = Date.now();
return next();
});
// Access in handler
app.get("/profile", (ctx) => {
const user = ctx.store.user;
const elapsed = Date.now() - ctx.store.startTime;
return ctx.json({ user, elapsed });
});Type Safety:
// Define store type (optional)
interface AppStore {
user?: User;
startTime?: number;
}
// Use with type assertion
app.get("/profile", (ctx) => {
const store = ctx.store as AppStore;
return ctx.json({ user: store.user });
});ctx.di
Access dependencies registered via app.set(). Uses Proxy for property access with full TypeScript autocomplete.
Type: DI (your Container type)
Examples:
type Container = {
db: Database;
userService: UserService;
config: { dbName: string; port: number };
};
const app = new App<Container>();
app.set("db", new Database());
app.set("userService", new UserService(db));
app.set("config", { dbName: "buntok_db", port: 1212 });
app.get("/users", (ctx) => {
// Property access
const users = ctx.di.db.getUsers();
// Destructuring
const { config } = ctx.di;
return ctx.json({
database: config.dbName,
data: users,
});
});Error Handling:
app.get("/data", (ctx) => {
try {
const users = ctx.di.db.getUsers();
return ctx.json({ data: users });
} catch (err) {
// Thrown if dependency not registered
return ctx.json({ error: "Database not configured" }, 500);
}
});ctx.body<T>()
Parse request body as JSON and return typed data.
Type Parameter:
| Type | Description |
|------|-------------|
| T | Expected body type (defaults to unknown) |
Returns: Promise<T>
Throws: Error if body is not valid JSON
Examples:
// Basic usage
app.post("/users", async (ctx) => {
const body = await ctx.body();
return ctx.json({ received: body });
});
// With type
interface CreateUserRequest {
name: string;
email: string;
}
app.post("/users", async (ctx) => {
const body = await ctx.body<CreateUserRequest>();
// body.name is string
// body.email is string
return ctx.json({ created: true, user: body });
});Error Handling:
app.post("/users", async (ctx) => {
try {
const body = await ctx.body<{ name: string }>();
return ctx.json({ success: true });
} catch {
return ctx.json({ error: "Invalid JSON body" }, 400);
}
});ctx.json(data, status?)
Return a JSON response.
Parameters:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| data | unknown | Yes | - | Data to serialize as JSON |
| status | number | No | 200 | HTTP status code |
Returns: Response
Examples:
// 200 OK (default)
return ctx.json({ message: "Success" });
// 201 Created
return ctx.json({ data: newUser }, 201);
// 400 Bad Request
return ctx.json({ error: "Invalid input" }, 400);
// 404 Not Found
return ctx.json({ error: "User not found" }, 404);Headers Set Automatically:
Content-Type: application/json
ctx.text(text, status?)
Return a plain text response.
Parameters:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| text | string | Yes | - | Response body text |
| status | number | No | 200 | HTTP status code |
Returns: Response
Examples:
// Simple text
return ctx.text("Hello, World!");
// With status
return ctx.text("Not Found", 404);
// HTML response
return ctx.text("<h1>Hello!</h1>");
// Note: Use ctx.json() for application/jsonHeaders Set Automatically:
Content-Type: text/plain
ctx.status(code)
Return an empty response with only a status code.
Parameters:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| code | number | Yes | HTTP status code |
Returns: Response
Use Cases:
- 204 No Content (successful DELETE)
- 301 Redirect (with Location header)
- 403 Forbidden
Examples:
// Delete success
app.delete("/users/:id", async (ctx) => {
await db.users.delete(ctx.params.id);
return ctx.status(204);
});
// Redirect
app.get("/old-path", (ctx) => {
return new Response(null, {
status: 301,
headers: { Location: "/new-path" },
});
});Built-in Middlewares
CORS
Handles Cross-Origin Resource Sharing.
Import:
import { cors } from "buntok/middlewares/cors";Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| origin | string \| string[] \| Function | "*" | Allowed origins |
| methods | string[] | ["GET","POST","PUT","DELETE","PATCH","OPTIONS"] | Allowed methods |
| headers | string[] | ["Content-Type","Authorization","x-api-key"] | Allowed headers |
| credentials | boolean | false | Allow credentials |
Examples:
// Allow all origins (default)
app.use(cors());
// Restrict to specific origins
app.use(cors({
origin: ["https://example.com", "https://app.example.com"],
}));
// Dynamic origin check
app.use(cors({
origin: (origin) => origin.endsWith(".example.com"),
}));
// With credentials (cookies, auth headers)
app.use(cors({
origin: "https://example.com",
credentials: true,
}));Headers Added:
| Header | Value |
|--------|-------|
| Access-Control-Allow-Origin | Allowed origin |
| Access-Control-Allow-Methods | Allowed HTTP methods |
| Access-Control-Allow-Headers | Allowed request headers |
| Access-Control-Allow-Credentials | "true" (if enabled) |
Validator
Validate request body and/or route params before hitting your handler.
Import:
import { validate, validateBody, validateParams } from "buntok/middlewares/validator";Schema Interface:
Any object with a .parse() method works:
interface ValidatorSchema {
parse: (data: unknown) => unknown;
}Compatible with: Zod, Valibot, Yup, custom schemas.
validateBody(schema)
Validate request body only.
import { validateBody } from "buntok/middlewares/validator";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18),
});
app.post("/users", validateBody(CreateUserSchema), (ctx) => {
const data = ctx.store.validatedBody;
return ctx.json({ created: true, data }, 201);
});validateParams(schema)
Validate route params only.
import { validateParams } from "buntok/middlewares/validator";
const IdSchema = {
parse: (data: unknown) => {
const { id } = data as Record<string, string>;
if (!id || id.length < 3) {
throw new Error("Invalid ID format");
}
return { id };
},
};
app.get("/users/:id", validateParams(IdSchema), (ctx) => {
const { id } = ctx.store.validatedParams;
return ctx.json({ id });
});validate({ body?, params? })
Validate both body and params.
import { validate } from "buntok/middlewares/validator";
app.put("/users/:id", validate({
body: CreateUserSchema,
params: IdSchema,
}), (ctx) => {
const { id } = ctx.store.validatedParams;
const body = ctx.store.validatedBody;
return ctx.json({ id, ...body });
});Validation Error Response (400):
{
"error": "Validation Failed",
"details": [
"body: Name is required",
"params: Invalid ID format"
]
}Rate Limiter
Limit request rate per IP or custom key.
Import:
import { rateLimiter, slidingWindowRateLimiter } from "buntok/middlewares/rate-limiter";Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| max | number | 100 | Maximum requests per window |
| windowMs | number | 60000 | Window duration in milliseconds |
| message | string | "Too many requests..." | Error message |
| statusCode | number | 429 | HTTP status code |
| headers | boolean | true | Add rate limit headers |
| keyGenerator | Function | IP-based | Custom key function |
| skip | Function | - | Skip rate limiting |
Examples:
import { rateLimiter } from "buntok/middlewares/rate-limiter";
// Default: 100 requests per minute
app.use(rateLimiter());
// Custom limits
app.use(rateLimiter({
max: 50,
windowMs: 60000, // 1 minute
message: "Rate limit exceeded",
}));
// Custom key (e.g., by user ID)
app.use(rateLimiter({
keyGenerator: (ctx) => ctx.store.userId || ctx.ip,
}));
// Sliding window (more accurate)
import { slidingWindowRateLimiter } from "buntok/middlewares/rate-limiter";
app.use(slidingWindowRateLimiter({ max: 100 }));Response Headers:
| Header | Description |
|--------|-------------|
| X-RateLimit-Limit | Maximum requests allowed |
| X-RateLimit-Remaining | Requests remaining in window |
| X-RateLimit-Reset | Window reset timestamp |
| Retry-After | Seconds until next request (when limited) |
Request ID
Add unique request ID to every request.
Import:
import { requestId } from "buntok/middlewares/request-id";Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| header | string | "x-request-id" | Header name |
| generator | Function | UUID | Custom ID generator |
| store | boolean | true | Store in ctx.store |
| storeKey | string | "requestId" | Property name in store |
Examples:
import { requestId } from "buntok/middlewares/request-id";
// Default
app.use(requestId());
// Custom header and generator
import { shortId } from "buntok/middlewares/request-id";
app.use(requestId({
header: "x-trace-id",
generator: shortId,
}));
// Access in handler
app.get("/api", (ctx) => {
const requestId = ctx.store.requestId;
return ctx.json({ requestId });
});Response Time
Measure and add response time to headers.
Import:
import { responseTime } from "buntok/middlewares/response-time";Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| header | string | "x-response-time" | Header name |
| format | "ms" \| "s" | "ms" | Time format |
| store | boolean | false | Store in ctx.store |
| storeKey | string | "responseTime" | Property name in store |
Examples:
import { responseTime } from "buntok/middlewares/response-time";
// Default (milliseconds)
app.use(responseTime());
// Seconds format
app.use(responseTime({ format: "s" }));
// Store in ctx.store for logging
app.use(responseTime({ store: true }));
app.get("/api", (ctx) => {
// Access response time after handler executes
return ctx.json({ message: "Hello" });
});Health Check
Register health check endpoint.
Import:
import { healthCheck, createDatabaseCheck } from "buntok/middlewares/health-check";Examples:
import { healthCheck } from "buntok/middlewares/health-check";
// Basic health check
healthCheck(app);
// Custom path
healthCheck(app, { path: "/api/health" });
// With database check
import { createDatabaseCheck } from "buntok/middlewares/health-check";
healthCheck(app, {
check: createDatabaseCheck(async () => {
// Your database ping logic
await db.query("SELECT 1");
return true;
}),
});
// Multiple checks
import { createHealthCheck } from "buntok/middlewares/health-check";
healthCheck(app, {
check: createHealthCheck([
{ name: "database", check: async () => { /* db check */ return true; } },
{ name: "redis", check: async () => { /* redis check */ return true; } },
]),
});Response Example:
{
"status": "healthy",
"timestamp": "2024-01-15T10:30:00.000Z",
"uptime": 3600,
"checks": {
"database": { "status": "up", "duration": 5 }
}
}SSE (Server-Sent Events)
Real-time data streaming to clients.
Import:
import { SSE, createSSE } from "buntok";Examples:
import { SSE } from "buntok";
// Basic SSE endpoint
app.get("/events", (ctx) => {
const sse = new SSE(ctx.request);
// Send events
sse.sendEvent("message", { text: "Hello!" });
sse.sendData("Simple data");
sse.sendWithId(1, { id: 1, data: "With ID" });
return sse.connect();
});
// With options
app.get("/stream", (ctx) => {
const sse = new SSE(ctx.request, {
sendInitial: true,
initialEvent: "connected",
retry: 5000, // Client reconnect after 5s
});
// Send heartbeat every 30s (automatic)
return sse.connect();
});
// Using helper function
import { createSSE } from "buntok";
app.get("/events", (ctx) => {
const sse = createSSE(ctx.request);
sse.sendEvent("update", { time: Date.now() });
return sse.connect();
});SSE Methods:
| Method | Description |
|--------|-------------|
| send(message) | Send full message object |
| sendData(data) | Send simple data |
| sendEvent(event, data) | Send named event |
| sendWithId(id, data) | Send with ID (for resumption) |
| close() | Close connection |
| isConnected | Check if connected |
Cookie Helpers
Parse and manage cookies.
Import:
import { getCookie, setCookie, deleteCookie, parseCookies } from "buntok/helpers/cookie";Examples:
import { getCookie, setCookie, deleteCookie } from "buntok/helpers/cookie";
// Get cookie in handler
app.get("/api", (ctx) => {
const token = ctx.getCookie("auth_token");
// Or use helper
// const token = getCookie(ctx.request, "auth_token");
return ctx.json({ token });
});
// Set cookie on response
app.post("/login", async (ctx) => {
const response = ctx.json({ success: true });
return setCookie(response, "auth_token", "xyz123", {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 86400, // 1 day
});
});
// Delete cookie
app.post("/logout", (ctx) => {
const response = ctx.json({ success: true });
return deleteCookie(response, "auth_token");
});
// Parse all cookies
app.get("/cookies", (ctx) => {
const cookies = ctx.getCookies(); // Or parseCookies(request)
return ctx.json({ cookies });
});File Uploads
Parse and validate multipart/form-data uploads using customizable storage drivers. Features automatic size and MIME-type validation.
Import:
import { uploader, LocalDiskStorage, parseUploads } from "buntok";As a Middleware:
Automatically parses files and places them in ctx.store.files, and standard form fields in ctx.store.fields.
app.post("/avatar",
uploader({
storage: new LocalDiskStorage("./uploads"),
maxFileSize: 5 * 1024 * 1024, // 5MB
allowedMimeTypes: ["image/jpeg", "image/png"]
}),
(ctx) => {
// Access uploaded files
const file = ctx.store.files[0];
return ctx.json({
success: true,
file: file.filename
});
}
);Manual Parsing (Helper Method):
Parse uploads directly inside your handler for maximum flexibility.
app.post("/documents", async (ctx) => {
const result = await parseUploads(ctx, {
storage: new LocalDiskStorage("./storage/docs"),
maxFileSize: 10 * 1024 * 1024
});
if (!result.success) {
return ctx.error(result.error);
}
// Text inputs from the same form are separated in fields
const { title } = result.fields;
return ctx.json({ title, files: result.files });
});Custom Storage Drivers (Cloudinary, AWS S3, etc):
Buntok uses a plugin-based architecture for storage. You can easily build your own driver for third-party providers (like Cloudinary or S3) by implementing the StorageDriver interface.
Here is a ready-to-use example for Cloudinary:
import { StorageDriver, UploadedFile, uploader } from "buntok";
import { v2 as cloudinary } from "cloudinary";
export class CloudinaryStorage implements StorageDriver {
async handleFile(file: File, filename: string): Promise<UploadedFile> {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return new Promise((resolve, reject) => {
cloudinary.uploader.upload_stream(
{ public_id: filename },
(error, result) => {
if (error) return reject(error);
resolve({
originalName: file.name,
filename: result!.public_id,
size: file.size,
type: file.type,
path: result!.secure_url, // Direct image URL
});
}
).end(buffer);
});
}
}
// Usage:
app.post("/avatar", uploader({ storage: new CloudinaryStorage() }), (ctx) => {
return ctx.json({ url: ctx.store.files[0].path });
});Enterprise Features
Buntok is built to scale. It comes with built-in modules for common enterprise requirements.
Caching
Buntok includes a robust caching system using a Driver pattern. By default, it uses MemoryCacheDriver. It supports atomic operations and advanced key retrieval.
import { Cache, MemoryCacheDriver } from "buntok";
// Initialize the Cache
const cache = new Cache(new MemoryCacheDriver());
// Set a value with a 60-second TTL
await cache.set("user:1", { name: "John" }, 60);
// getOrSet: fetch from DB if not in cache, then cache it for 5 mins
const user = await cache.getOrSet("user:2", () => db.findUser(2), 300);
// Atomic counters
const views = await cache.increment("post:1:views");
// Pattern deletion (e.g. clear all session keys)
await cache.deletePattern("session:*");Background Queues
Handle long-running tasks asynchronously using the Queue system. Features automatic retries, backoff strategies, delays, and priority queuing.
import { Queue } from "buntok";
// Initialize the Queue with options
const emailQueue = new Queue("email", {
maxRetries: 3,
retryDelay: 1000,
backoff: "exponential"
});
// Define a worker
emailQueue.process(async (job) => {
console.log(`[Attempt ${job.attempt + 1}] Sending email to:`, job.data.to);
});
// Add jobs to the queue with options
await emailQueue.add(
{ to: "[email protected]", subject: "Hello" },
{ delay: 5000, priority: 10 } // Wait 5 seconds, high priority
);Task Scheduling
Run recurring background jobs using standard cron syntax and the @CronJob() decorator.
Unlike other frameworks, @CronJob safely binds to the class instance, allowing you to access injected services via this.
import { Controller, CronJob } from "buntok";
@Controller("/tasks")
export class TaskController {
constructor(private readonly cache: Cache) {}
// Runs every day at midnight
@CronJob("0 0 * * *")
async performCleanup() {
// \`this\` works perfectly!
await this.cache.deletePattern("tmp:*");
console.log("Cleanup complete!");
}
}Auth Guards
Protect your endpoints using granular Auth Guards and the @UseGuard() decorator.
import { Controller, Get, UseGuard, type GuardFn } from "buntok";
const IsAdmin: GuardFn = async (ctx) => {
const user = await ctx.di.get("user");
return user?.role === "admin";
};
@Controller("/admin")
export class AdminController {
@Get("/dashboard")
@UseGuard(IsAdmin)
async dashboard(ctx) {
return ctx.json({ message: "Welcome Admin" });
}
}AI & LLM Integration (AI Ready)
Buntok natively supports the Vercel AI SDK Data Stream Protocol, making it the perfect backend for Generative UI, chatbots, and autonomous agents.
import { Router, streamAI, injectSystemPrompt, AICache, MemoryCacheDriver } from "buntok";
import { OpenAI } from "openai";
const app = new Router();
const openai = new OpenAI();
const aiCache = new AICache(new MemoryCacheDriver());
app.post("/api/chat", async (ctx) => {
const { messages } = await ctx.body();
// 1. Semantic Cache: Check if we've seen this exact conversation
const cached = await aiCache.get(messages);
if (cached) return ctx.json({ role: "assistant", content: cached });
// 2. Guardrails: force system prompt & prevent injection
const securedMessages = injectSystemPrompt(messages, "You are a helpful assistant.");
// 3. Fetch from OpenAI (Stream mode)
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
stream: true,
messages: securedMessages,
});
// 4. Automatically stream via Vercel AI Protocol (0:"text")
return streamAI(ctx, response, {
onCompletion: async (fullText) => await aiCache.set(messages, fullText)
});
});Examples
Basic CRUD API
import { App } from "buntok";
const app = new App();
// In-memory storage
const users: Array<{ id: string; name: string; email: string }> = [];
// List all users
app.get("/users", (ctx) => {
return ctx.json({ data: users });
});
// Get user by ID
app.get("/users/:id", (ctx) => {
const user = users.find((u) => u.id === ctx.params.id);
if (!user) {
return ctx.json({ error: "User not found" }, 404);
}
return ctx.json({ data: user });
});
// Create user
app.post("/users", async (ctx) => {
const body = await ctx.body<{ name: string; email: string }>();
const user = {
id: crypto.randomUUID(),
...body,
};
users.push(user);
return ctx.json({ data: user }, 201);
});
// Update user
app.put("/users/:id", async (ctx) => {
const index = users.findIndex((u) => u.id === ctx.params.id);
if (index === -1) {
return ctx.json({ error: "User not found" }, 404);
}
const body = await ctx.body<{ name: string; email: string }>();
users[index] = { ...users[index], ...body };
return ctx.json({ data: users[index] });
});
// Delete user
app.delete("/users/:id", (ctx) => {
const index = users.findIndex((u) => u.id === ctx.params.id);
if (index === -1) {
return ctx.json({ error: "User not found" }, 404);
}
users.splice(index, 1);
return ctx.status(204);
});Authentication Middleware
import { App } from "buntok";
import type { Middleware } from "buntok";
const app = new App();
// Auth middleware
const auth: Middleware = async (ctx, next) => {
const token = ctx.request.headers.get("Authorization");
if (!token) {
return ctx.json({ error: "No token provided" }, 401);
}
try {
// Verify token (example with JWT)
const user = await verifyToken(token);
ctx.store.user = user;
return next();
} catch {
return ctx.json({ error: "Invalid token" }, 401);
}
};
// Admin-only middleware
const adminOnly: Middleware = async (ctx, next) => {
if (ctx.store.user?.role !== "admin") {
return ctx.json({ error: "Admin access required" }, 403);
}
return next();
};
// Protected routes
app.get("/profile", auth, (ctx) => {
return ctx.json({ user: ctx.store.user });
});
// Admin routes
app.get("/admin/dashboard", auth, adminOnly, (ctx) => {
return ctx.json({ message: "Welcome, admin!" });
});Route Groups
import { App } from "buntok";
const app = new App();
// API v1 group
const v1 = app.group("/api/v1");
// Auth middleware for all v1 routes
v1.use(async (ctx, next) => {
const token = ctx.request.headers.get("Authorization");
if (!token) return ctx.json({ error: "Unauthorized" }, 401);
return next();
});
// GET /api/v1/users
v1.get("/users", (ctx) => {
return ctx.json({ users: [] });
});
// POST /api/v1/users
v1.post("/users", async (ctx) => {
const body = await ctx.body();
return ctx.json({ created: true, data: body }, 201);
});
// Nested group
const admin = v1.group("/admin");
// GET /api/v1/admin/settings
admin.get("/settings", (ctx) => {
return ctx.json({ settings: {} });
});
// Public routes (no auth)
app.get("/", (ctx) => {
return ctx.json({ message: "Public API" });
});Validation with Zod
import { App } from "buntok";
import { validate, validateBody } from "buntok/middlewares/validator";
import { z } from "zod";
const app = new App();
// Define schemas
const CreateUserSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email format"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
const UserIdSchema = z.object({
id: z.string().uuid("Invalid user ID"),
});
// Create user with validation
app.post("/users", validateBody(CreateUserSchema), async (ctx) => {
const data = ctx.store.validatedBody;
// data is fully typed and validated
const user = await db.users.create(data);
return ctx.json({ data: user }, 201);
});
// Get user with param validation
app.get("/users/:id", validate({ params: UserIdSchema }), (ctx) => {
const { id } = ctx.store.validatedParams;
return db.users.find(id);
});Static Files + SPA
import { App } from "buntok";
const app = new App();
// Serve static assets
app.static("/assets", "./public");
// SPA fallback - serve index.html for all other routes
app.get("*", (ctx) => {
return new Response(Bun.file("./public/index.html"));
});Configuration
Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| PORT | 1212 | Server port |
| NODE_ENV | development | Environment mode |
| LOG_DIR | - | Directory for log files |
| LOG_REQUESTS | true | Set to false to disable request logging (useful for benchmarks) |
Logger Behavior
| Environment | Log Level | Format | |-------------|-----------|--------| | Development | DEBUG, INFO, WARN, ERROR | Colored text | | Production | WARN, ERROR | JSON |
Examples:
# Custom port
PORT=8080 bun run src/index.ts
# Production mode (WARN+ERROR only, JSON format)
NODE_ENV=production bun run src/index.ts
# Enable file logging
LOG_DIR=./logs bun run src/index.ts
# Disable request logging (for benchmarks)
LOG_REQUESTS=false bun run src/index.tsTypeScript Configuration
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"types": ["bun"],
"strict": true,
"moduleResolution": "bundler",
"noEmit": true
}
}Project Structure
my-app/
├── src/
│ └── index.ts # Application entry point
├── public/
│ ├── favicon.ico # Favicon (auto-served)
│ └── logo.svg # Static assets
├── .vscode/
│ └── settings.json # VS Code + Biome config
├── biome.json # Linter/formatter config
├── tsconfig.json # TypeScript config
└── package.jsonPerformance
Buntok is built for speed. Here are benchmark results using autocannon with 50 concurrent connections:
Production Mode (NODE_ENV=production)
| Test | Req/sec | Latency avg | Latency p99 | Throughput |
|------|---------|-------------|-------------|------------|
| Plain text | 15,659 | 2.72ms | 10ms | 1.72 MB/s |
| JSON | 15,981 | 2.65ms | 10ms | 2.48 MB/s |
| Route params (/user/:id) | 17,542 | 2.38ms | 10ms | 2.48 MB/s |
| Query string | 18,955 | 2.16ms | 7ms | 2.64 MB/s |
| POST JSON 1 KB | 15,738 | 2.68ms | 10ms | 2.25 MB/s |
| Middleware 5 layers | 16,596 | 2.53ms | 10ms | 2.23 MB/s |
| Static file 100 KB | 4,717 | 10.15ms | 28ms | 461 MB/s |
Development Mode
| Test | Req/sec | Latency avg | Latency p99 | Throughput |
|------|---------|-------------|-------------|------------|
| Plain text | 6,918 | 6.81ms | 29ms | 0.76 MB/s |
| JSON | 6,142 | 7.65ms | 35ms | 0.95 MB/s |
| Route params (/user/:id) | 5,157 | 9.28ms | 45ms | 0.73 MB/s |
| Query string | 7,377 | 6.29ms | 25ms | 1.03 MB/s |
| POST JSON 1 KB | 6,257 | 7.51ms | 35ms | 0.90 MB/s |
| Middleware 5 layers | 8,060 | 5.71ms | 23ms | 1.08 MB/s |
| Static file 100 KB | 2,357 | 21.05ms | 67ms | 230 MB/s |
Note: Production mode only logs WARN+ERROR level (JSON format), while development mode logs all levels (colored text). The performance difference is primarily due to reduced I/O overhead in production.
Run Benchmarks
bun run bench # Development mode
bun run bench:prod # Production mode⚡ Performance Benchmark
Buntok is designed to give you the architectural elegance of heavy enterprise frameworks (like NestJS) without sacrificing the raw, face-melting speed of the Bun runtime.
In local benchmark tests (100 concurrent connections, 10 seconds, measured via bombardier), Buntok stands shoulder-to-shoulder with the fastest minimal web frameworks in the ecosystem. You get full Object-Oriented Dependency Injection and Decorators with zero performance penalty.
| Framework | Plaintext (req/sec) | JSON (req/sec) | Dynamic Route (req/sec) | | :--- | :--- | :--- | :--- | | Buntok | ~18,527 | ~18,221 | ~18,099 | | Elysia | ~18,772 | ~18,150 | ~18,286 | | Hono | ~18,794 | ~18,373 | ~18,271 | | Fastify | ~18,097 | ~18,549 | ~18,101 | | Express | ~19,286* | ~17,455 | ~17,082 |
(Note: Benchmark conducted on a standardized local test VM. The numbers demonstrate that Buntok easily matches and often exceeds the throughput of minimal functional routers despite offering a heavy OOP Developer Experience).
📖 Complete Documentation
Code Generation
Buntok includes a CLI tool for generating code (schema, repository, service, controller) using Drizzle ORM.
Generate All Files
bunx buntok create <entity>Example:
bunx buntok create userThis generates:
src/db/schemas/user.ts- Drizzle schemasrc/repositories/user.repository.ts- Repository patternsrc/services/user.service.ts- Service layersrc/controllers/user.controller.ts- Controller with CRUD
Generate Specific Files
bunx buntok create <entity> --schema # Only Drizzle schema
bunx buntok create <entity> --repo # Only repository
bunx buntok create <entity> --service # Only service
bunx buntok create <entity> --controller # Only controllerCombine Options
bunx buntok create user --repo --service # Repository + Service onlyAfter Generation
- Add columns to your schema in
src/db/schemas/<entity>.ts - Run migrations:
bun run db:push # Push schema to database bun run db:generate # Generate migration files bun run db:migrate # Run migrations bun run db:studio # Open Drizzle Studio - Register routes in
src/index.ts
Scripts
bun run dev # Start with hot reload
bun run start # Start production server
bun run bench # Run benchmarks (development)
bun run bench:prod # Run benchmarks (production)
bun run check # Biome check + format
bun run format # Format code only
bun run lint # Lint code onlyLicense
MIT
