---
description: Fastify 5 API conventions
globs: ["src/**/*.ts"]
alwaysApply: false
---

# Fastify 5

- Build the app in `src/app.ts` as `export async function buildApp(opts)` returning a Fastify instance; `src/server.ts` calls `listen`. Tests use `app.inject()`, not a running server.
- Organize features as plugins (`src/routes/orders/index.ts` exporting an async plugin). Use `fastify-plugin` only for plugins that must share decorators with the parent (DB client, auth); otherwise keep encapsulation.
- Every route declares a `schema` (body, querystring, params, response) with TypeBox (`@sinclair/typebox` + `TypeBoxTypeProvider`) or zod via `fastify-type-provider-zod`. Response schemas both document and strip extra fields.
- Decorate shared services (`app.decorate('db', db)`) with TypeScript declaration merging for types.
- Auth in an `onRequest`/`preHandler` hook or `@fastify/jwt`; don't repeat checks in handlers.
- Errors: throw `httpErrors` from `@fastify/sensible` or set `reply.code()`; one `setErrorHandler` for formatting. Don't leak stack traces.
- Use the built-in pino logger (`request.log.info({ orderId }, 'created')`); no `console.log`.
- Register `@fastify/helmet`, `@fastify/cors` (explicit origins), and `@fastify/rate-limit` where relevant.
- Async handlers return the payload; don't mix `return` with `reply.send()`.
- Close resources in an `onClose` hook.

## Done when
`tsc --noEmit`, lint, and `app.inject()` tests for each new route (success and validation failure) pass.
