serve
bun add @stopcock/serveHTTP 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/createReal-world patterns
Section titled “Real-world patterns”CRUD routes
Section titled “CRUD routes”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)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)}Validated request body
Section titled “Validated request body”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)}Context
Section titled “Context”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>Response builders
Section titled “Response builders”json(data: unknown, status?: number): Responsetext(body: string, status?: number): Responsehtml(body: string, status?: number): Responsestatus(code: number): Responseredirect(url: string, status?: number): Responseheaders(response: Response, headers: Record<string, string>): Responsecookie(response: Response, name: string, value: string, opts?: CookieOptions): ResponseMiddleware
Section titled “Middleware”cors(opts?: CorsOptions): Middlewarebody(): Middlewaretimeout(ms: number): MiddlewareRouting
Section titled “Routing”scanRoutes(dir: string): Promise<Route[]>Router(routes: Route[]): (req: Request) => Response | Promise<Response>runPipeline(handler: Handler, middleware: Middleware[]): HandlerContext helpers
Section titled “Context helpers”createContext(req: Request, params: Record<string, string>): CtxextractPath(url: string): stringextractQuery(url: string): Record<string, string>ensureQuery(ctx: Ctx, key: string): string