Skip to content

serve

Terminal window
bun add @stopcock/serve

HTTP framework for Bun with filesystem-based routing. Built directly on Bun.serve().

Drop .ts files in routes/ and they become endpoints. [id].ts for params, .post.ts for method dispatch. scanRoutes(dir) picks them all up, no manual registration needed. Middleware (cors(), body(), timeout()) works per-route or globally.

import { serve, scanRoutes, json, body, cors, timeout } from '@stopcock/serve'
const routes = await scanRoutes('./routes')
serve({
routes,
middleware: [cors(), body(), timeout(5000)],
port: 3000,
})
routes/
users/
index.ts → GET /users
[id].ts → GET /users/:id
create.post.ts → POST /users/create
routes/
users/
index.ts → GET /users (list)
create.post.ts → POST /users (create)
[id].ts → GET /users/:id (read)
[id].put.ts → PUT /users/:id (update)
[id].delete.ts → DELETE /users/:id (delete)
routes/users/[id].ts
import { json } from '@stopcock/serve'
export default async (ctx) => {
const user = await db.users.find(ctx.params.id)
if (!user) return json({ error: 'not found' }, 404)
return json(user)
}
routes/users/create.post.ts
import { json, body } from '@stopcock/serve'
import { V, compile } from '@stopcock/validate'
const CreateUser = V.object({
name: V.string().min(1),
email: V.string().email(),
})
const validate = compile(CreateUser)
export const middleware = [body()]
export default async (ctx) => {
const result = validate(ctx.body)
if (!result.success) return json(result.errors, 400)
const user = await db.users.create(result.data)
return json(user, 201)
}

Every handler receives a Ctx with the request and extracted params.

type Ctx = {
req: Request
params: Record<string, string>
query: Record<string, string>
body: unknown
headers: Headers
}
type Handler = (ctx: Ctx) => Response | Promise<Response>
json(data: unknown, status?: number): Response
text(body: string, status?: number): Response
html(body: string, status?: number): Response
status(code: number): Response
redirect(url: string, status?: number): Response
headers(response: Response, headers: Record<string, string>): Response
cookie(response: Response, name: string, value: string, opts?: CookieOptions): Response
cors(opts?: CorsOptions): Middleware
body(): Middleware
timeout(ms: number): Middleware
scanRoutes(dir: string): Promise<Route[]>
Router(routes: Route[]): (req: Request) => Response | Promise<Response>
runPipeline(handler: Handler, middleware: Middleware[]): Handler
createContext(req: Request, params: Record<string, string>): Ctx
extractPath(url: string): string
extractQuery(url: string): Record<string, string>
ensureQuery(ctx: Ctx, key: string): string