File-based routing
The file's location is the URL. api/customers/[id].ts becomes /api/customers/{id}. Nothing to register, nothing to hardcode.
The framework your AI can’t make a mess of. Drop a file, get a typed, documented, validated endpoint.
bun add @prehoy/baguette// api/customers/[id].ts
import { defineRoute, z } from "@prehoy/baguette";
export default defineRoute({
method: "get",
request: { params: z.object({ id: z.string() }) },
response: z.object({ id: z.string(), name: z.string() }),
handler: (c, { params }) =>
c.json({ id: params.id, name: "Ada" }),
});One zod declaration → validation, types, docs, and an error funnel.
Scaffold, run, hit it. No config, no boilerplate, no wiring — the docs generate themselves and every response is validated and typed.
bun create baguette my-apiPoint serve() at a directory. Every file that default-exports a defineRoute is mounted at the path its location implies. That’s the entire wiring step.
// server.ts
import { serve } from "@prehoy/baguette";
serve({ routesDir: "./api" });
// routes loaded · validation on · docs at /api/docsBad input never reaches your handler — 400 automatically.
params, query and body arrive fully typed. No casts.
The spec and the docs page write themselves.
Unhandled throws become a clean 500. One place to look.
The file's location is the URL. api/customers/[id].ts becomes /api/customers/{id}. Nothing to register, nothing to hardcode.
Declare request and response as zod schemas once. Validation, inferred handler types, and the OpenAPI spec all fall out of that single declaration.
A live Scalar UI at /api/docs and the raw spec at /api/doc — generated from your schemas, never hand-wired, never stale.
Declarative per-route auth, built-in rate limiting (brute-force + email-bomb protection), security headers, a CORS footgun guard, and body limits — one flag each.
onBoot and onShutdown hooks with graceful SIGTERM draining, static/SPA serving, and a typed defineEnv that replaces your hand-written env.ts.
Opt-in WebSockets with a built-in room/channel pub/sub — broadcast from any handler, cron, or automation. Bun-native, no socket.io. Off until you set ws.
The framework imports no ORM. Bring Prisma, Drizzle, or raw SQL. Nothing in the hot path you didn't put there.
cron/ for scheduled jobs, queues/ for bee-queue background jobs, automations/ for LISTEN/NOTIFY handlers, and React email templates with a browser preview. Each off until you opt in.
One obvious way to do each thing, enforced. A shipped clean-code contract, a baguette/eslint preset, and baguette check keep the codebase boring and typed.
Generated code drifts because there are a hundred ways to do everything. baguette ships one. A clean-code contract in AGENTS.md, a baguette/eslint preset, and a checker turn those conventions into CI failures — so an agent physically can’t merge the mess.
$ baguette check
api/customers/create.ts
warn hardcoded path — derive it from the file location
warn manual c.req.json() — declare a request.body schema
api/legacy/proxy.ts
error 'as any' in app code — the framework is missing a type
warnings · error · routes cleanbaguette check runs in CI. Clean code passes; clever code doesn’t.
baguette is built by Prehoy Industries. Deploy your API managed on Berth, or bring us in to build and run the whole backend and infrastructure.