serve
serve() is the entire wiring step. Point it at your routes directory; it imports every route in parallel, mounts each at the path its file location implies, wires up the OpenAPI docs, and starts a Bun server.
// server.ts
import { serve } from "@prehoy/baguette";
serve({ routesDir: "./api" });
// routes loaded · validation on · docs at /api/docs
bun run server.ts
What serve does
- Loads routes in parallel at boot — route files are imported concurrently, not one by one.
- Mounts by convention — each
defineRoutelands at the path derived from its file location. - Serves the docs — a Scalar UI at
/api/docsand the raw OpenAPI spec at/api/doc. - Installs the error funnel — unhandled throws become a clean
500. - Enables
reusePort— so you can run multiple instances behind the same port. - Logs requests — a basic request log that never stringifies the body.
Middleware, auth & production options
serve() (and createApp()) take the options every real deployment needs — all off by default, one flag to turn on:
| Option | What it does |
|---|---|
auth | A resolver (c) => user | null. Routes marked auth: true run it — 401 on falsy, else it sets c.get("user"). See defineRoute. |
securityHeaders: true | HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and more. |
cors | true → origin:"*". For cookies/Bearer from a browser use { reflect: true, credentials: true } — baguette refuses the invalid origin:"*" + credentials combo. |
bodyLimit | Max request body in bytes; larger → 413. |
rateLimitStore | Backing store for per-route rateLimit. Default in-memory; RATELIMIT_STORE=redis (+ REDIS_URL) shares it across replicas. |
spa | Serve a static SPA (spa: "./public") with an index.html deep-link fallback, mounted after the API. |
onApp(app) | Mount custom middleware/static on the Hono app before it listens. |
onBoot() | Run before the server accepts traffic — migrations, seed, warmup. |
onShutdown() | Run on SIGTERM/SIGINT after draining — close DB/cron. |
serve({
routesDir: "./api",
auth: async (c) => verifySession(c.req.header("authorization")),
securityHeaders: true,
cors: { reflect: true, credentials: true }, // browser + credentials, done right
bodyLimit: 1_000_000,
spa: "./public",
onBoot: () => migrate(),
onShutdown: () => db.end(),
});
Auto-wiring the optional layers
serve() wires the optional layers by convention — a directory existing is the on switch. Nothing is imported if its directory is absent:
- If
cron/exists, scheduled jobs start. - If
automations/exists andAUTOMATIONS=true, event handlers start.
This keeps a plain HTTP app free of any Postgres or scheduling code it never asked for.
Environment
Validate and type the environment once at boot with defineEnv (a zod schema) instead of reading Bun.env.X in scattered places:
import { defineEnv, z } from "@prehoy/baguette";
export const env = defineEnv(z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
}));
// env.PORT is a number, env.DATABASE_URL a validated string — typed everywhere.
A missing or malformed variable fails fast at startup, listing every problem, instead of at 3am. (validateEnv(["A", "B"]) remains for a plain required-keys check.)
Related
- Getting started — the full install-to-running walkthrough.
- Cron and Automations — the optional layers serve auto-wires.