baguette

Security

baguette ships the controls every API needs as declarative options — no middleware wiring, no forgotten requireAuth. Turn each on with a flag.

Authentication

Mark a route auth: true and give serve() a resolver. It runs before the handler; a falsy result is a 401, otherwise the user is typed on the context.

// server.ts
serve({ routesDir: "./api", auth: (c) => verifySession(c.req.header("authorization")) });

// api/orders/create.ts
export default defineRoute({
  method: "post",
  auth: true,
  handler: (c) => {
    const user = c.get("user"); // typed — augment BaguetteUser
    return c.json({ ownerId: user.id });
  },
});

A route with auth: true and no resolver throws at boot — you can't ship an unprotected "protected" route.

Rate limiting

Cap brute-force and email-bombing per route. Runs before auth, keyed by client IP (honouring X-Forwarded-For behind a proxy):

export default defineRoute({
  method: "post",
  rateLimit: { limit: 5, window: "15m" }, // 5 attempts / 15 min / IP → 429
  auth: true,
  handler: login,
});

Bucket by something other than IP — e.g. the target email, to stop bombing one address — with key:

rateLimit: { limit: 3, window: "1h", key: (c) => c.req.query("email") ?? clientIp(c) }

Counters are in-memory by default; set RATELIMIT_STORE=redis (+ REDIS_URL) to share limits across replicas. Over the limit → 429 with Retry-After.

Security headers

securityHeaders: true adds HSTS, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy, and more:

serve({ routesDir: "./api", securityHeaders: true });

CORS, done right

The default is origin: "*" with no credentials. For a browser sending cookies or a Bearer token you must reflect the origin — baguette refuses the invalid origin:"*" + credentials combo and gives you a valid recipe:

serve({ cors: { reflect: true, credentials: true } });

Body-size limit

Cap request bodies to shrink the DoS surface; larger → 413:

serve({ bodyLimit: 1_000_000 }); // 1 MB

Errors don't leak

An unhandled throw becomes a generic 500 with a process_id only. The full error — message, stack, upstream payloads — goes to the logs, correlated by that id. Nothing internal is echoed to the client, so error responses aren't a recon surface.

  • defineRouteauth, rateLimit, middleware per route.
  • servesecurityHeaders, cors, bodyLimit, rateLimitStore.
  • Clean-code contract — "protect routes declaratively" is enforced.