A production-ready starter monorepo template for building HTTP APIs and WebSocket backends with Bun, TypeScript, and PostgreSQL.
| Layer | Technology |
|---|---|
| Runtime | Bun >= 1.3.3 |
| HTTP/WebSocket | @bunkit/server — custom type-safe framework |
| Error handling | @bunkit/result — Result pattern |
| Database | PostgreSQL + Drizzle ORM |
| Validation | Zod v4 |
| Auth | JWT via jose |
| API docs | OpenAPI 3.1 via zod-openapi |
| Code quality | Biome |
| Frontend (example) | React 19 + Rolldown-Vite + TailwindCSS 4 + TanStack Query |
The included React frontend is a reference implementation. Replace it with any framework or no framework, or just delete it.
- Bun >= 1.3.3
- PostgreSQL >= 14
git clone https://github.com/gegehprast/bunkit my-project && cd my-project
bun installCreate apps/backend/.env.local:
NODE_ENV=development
PORT=3001
HOST=0.0.0.0
DATABASE_URL=postgresql://user:password@localhost:5432/myapp_dev
JWT_SECRET=your-super-secret-jwt-key-minimum-32-characters-long
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=your-super-secret-refresh-key-minimum-32-characters
JWT_REFRESH_EXPIRES_IN=30d
CORS_ORIGIN=http://localhost:5173
LOG_LEVEL=debugbun run backend:db:generate # generate migrations from schemas
bun run backend:db:migrate # apply migrations
bun run backend:dev # backend on http://localhost:3001
bun run frontend:dev # frontend on http://localhost:5173API docs available at http://localhost:3001/docs when the backend is running.
bunkit/
├── apps/
│ ├── backend/
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ ├── auth/ # JWT authentication
│ │ │ ├── config/ # Environment config
│ │ │ ├── core/ # Server, logger, errors, shutdown
│ │ │ ├── db/ # Drizzle client, schemas, repositories
│ │ │ ├── middlewares/
│ │ │ └── routes/ # HTTP + WebSocket routes
│ │ ├── drizzle/ # Migrations
│ │ ├── scripts/ # Code generation scripts
│ │ └── tests/
│ └── frontend/ # Example React app (replaceable)
│ └── src/
│ ├── components/
│ ├── hooks/
│ ├── lib/
│ └── generated/ # Auto-generated types from backend
├── packages/
│ ├── server/ # @bunkit/server
│ └── result/ # @bunkit/result
└── scripts/
└── lint.ts
All commands run from the repository root.
| Command | Description |
|---|---|
bun run lint |
Lint all packages |
bun run check |
Biome check with auto-fix |
bun run format |
Format all code |
| Command | Description |
|---|---|
bun run backend:dev |
Start with hot reload |
bun run backend:start |
Start (production) |
bun run backend:typecheck |
Type check |
bun run backend:test |
Run tests |
bun run backend:db:generate |
Generate Drizzle migrations |
bun run backend:db:migrate |
Apply migrations |
bun run backend:db:studio |
Open Drizzle Studio |
bun run backend:openapi:generate |
Generate OpenAPI types → apps/frontend/src/generated |
bun run backend:ws-types:generate |
Generate WebSocket types → apps/frontend/src/generated |
| Command | Description |
|---|---|
bun run frontend:dev |
Start dev server |
bun run frontend:build |
Production build |
bun run frontend:preview |
Preview production build |
bun run frontend:typecheck |
Type check |
import { createRoute } from "@bunkit/server"
import { z } from "zod"
const TodoSchema = z.object({
id: z.string(),
title: z.string(),
completed: z.boolean(),
})
createRoute("GET", "/api/todos/:id")
.response(TodoSchema)
.handler(({ params }) => {
return { id: params.id, title: "Example", completed: false }
})All service and repository methods must return Result<T, E> — never throw.
import { ok, err, type Result } from "@bunkit/result"
function findUser(id: string): Result<User, AppError> {
const user = db.getUserById(id)
return user ? ok(user) : err(new NotFoundError(`User ${id} not found`))
}
const result = findUser("123")
.map(user => user.email)
.andThen(email => sendEmail(email))After modifying backend schemas, regenerate types for the frontend:
bun run backend:openapi:generate # REST API types
bun run backend:ws-types:generate # WebSocket message typesGenerated files are written to apps/frontend/src/generated/. Use them with openapi-fetch:
import createClient from "openapi-fetch"
import type { paths } from "@/generated/openapi"
const client = createClient<paths>({ baseUrl: "http://localhost:3001" })
const { data, error } = await client.GET("/api/todos/{id}", {
params: { path: { id: "123" } },
})import { createWebSocketRoute } from "@bunkit/server"
import { z } from "zod"
createWebSocketRoute("/ws/chat")
.onMessage("chat", z.object({ roomId: z.string(), message: z.string() }), ({ data, ws }) => {
ws.send({ type: "chat", ...data })
})The template ships with working examples that can be removed or replaced:
- Auth — registration, login, JWT access/refresh tokens, protected routes
- Todos — full CRUD with user-scoped data
- Chat — WebSocket rooms, message broadcasting, typing indicators, user presence
- Create a route file in
apps/backend/src/routes/ - Register it in
apps/backend/src/routes/index.ts - Run
bun run backend:openapi:generateto update frontend types
- Define schema in
apps/backend/src/db/schemas/ bun run backend:db:generate— generate migrationbun run backend:db:migrate— apply migration- Create a repository in
apps/backend/src/db/repositories/
Tests must be run from each package directory:
cd apps/backend && bun test
cd packages/server && bun test
cd packages/result && bun testRoot shortcuts: bun run backend:test, bun run server:test, bun run result:test
Backend — Bun runs TypeScript directly, no build step required:
# Set NODE_ENV=production, strong JWT secrets, production DATABASE_URL, correct CORS_ORIGIN
bun run backend:db:migrate
bun run backend:startFrontend:
bun run frontend:build # outputs to apps/frontend/dist/Deploy apps/frontend/dist/ to any static host (Vercel, Netlify, Cloudflare Pages, etc.).