For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/adapters/fastify.md.

Fastify

@dotpress/adapter-fastify runs your app on Fastify 5, on Node.js and AWS Lambda.

npm
yarn
pnpm
bun
deno
npm install @dotpress/core @dotpress/adapter-fastify fastify zod
import { createApp, defineRoute } from '@dotpress/core'
import { fastifyAdapter } from '@dotpress/adapter-fastify'

defineRoute({ path: '/hello', handler: () => ({ message: 'Hello' }) })

const app = await createApp({ adapter: fastifyAdapter() })
await app.listen(3000)

Options

OptionDescription
appAn existing Fastify instance. Fastify() by default.
beforeRoutes(app)Registers hooks, plugins and native routes. Hooks registered here apply to the Dotpress routes.
afterRoutes(app)Registers hooks, plugins and native routes in an encapsulated context: they do not apply to the Dotpress routes, except plugins wrapped with fastify-plugin.
bodyLimitMaximum request body size, in bytes. 1 MiB by default.

Fastify plugins and hooks

Register Fastify plugins and hooks in beforeRoutes, which can be async: they apply to the Dotpress routes too.

import helmet from '@fastify/helmet'

const app = await createApp({
  adapter: fastifyAdapter({
    beforeRoutes: async (app) => {
      await app.register(helmet)
    },
  }),
})

The instance is ready once createApp() resolves: register everything in beforeRoutes or afterRoutes.

An existing instance

The app of the Fastify example mounts the Dotpress routes on an existing instance, next to a native route, with a Fastify hook applied to both:

import { fastifyAdapter } from '@dotpress/adapter-fastify'
import { createApp, type Logger } from '@dotpress/core'
import Fastify from 'fastify'
import { createNoteStore } from './notes.ts'
import { notesRouter } from './routes.ts'

/**
 * Mounts the Dotpress routes on a Fastify instance, next to its native
 * routes. Used by the server (`server.ts`) and the Lambda handler
 * (`lambda.ts`).
 */
export const createNotesApp = (options: { logger?: Logger } = {}) => {
  // An existing instance: its plugins, hooks and native routes stay.
  const fastify = Fastify()
  fastify.get('/health', async () => ({ status: 'ok' }))

  return createApp({
    adapter: fastifyAdapter({
      app: fastify,
      // Hooks and plugins registered here apply to the Dotpress routes too.
      beforeRoutes: (app) => {
        app.addHook('onRequest', async (request, reply) => {
          reply.header('x-request-id', request.id)
        })
      },
    }),
    ...(options.logger && { logger: options.logger }),
    routers: [notesRouter(createNoteStore())],
  })
}

Behavior

  • Each Dotpress route is a native Fastify route. Its path (request.routeOptions.url, /users/:id) shows in the Fastify logs, metrics and traces, and a native route with the same method and path fails at startup. Routing still follows the Dotpress rules: trailing slashes ignored, HEAD on GET routes, 405 with Allow.
  • Bodies are read by Dotpress, with the same rules on every adapter: the Fastify body parsers only apply to native routes, and @fastify/multipart is not needed for uploads. preParsing hooks apply. Like Fastify, the adapter does not read the bodies of GET and HEAD requests.
  • Errors with a 4xx statusCode raised by Fastify or its plugins (@fastify/rate-limit, @fastify/sensible…) use the Dotpress error format and keep their status and message. Other errors are unexpected (500). The adapter takes over the not-found and error handlers of the instance: they apply to native routes too.
  • ctx.req.ip is request.ip: proxy headers are only used with the trustProxy option of Fastify, set when you create the instance.
  • Streamed responses (stream(), sse()) are written directly to the Node.js response, so that their headers are sent at once: onSend hooks, such as @fastify/compress, do not apply to them.
  • ctx.raw is the Fastify request.
  • listen(port, host?) listens on every interface without host, like Node.js. close() closes the instance (its onClose hooks), waits for pending requests and closes the streamed responses.

Testing with inject()

Besides the memory adapter, Fastify tests can use inject(), which goes through the Fastify hooks and plugins without opening a port:

const adapter = fastifyAdapter()
await createApp({ adapter, routers })
const res = await adapter.native().inject('/hello')

On AWS Lambda

The same app runs on AWS Lambda with createLambdaHandler() of @dotpress/adapter-fastify/lambda: see AWS Lambda.