Fastify hat sich 2026 als das schnellste produktionsreife Node.js-Framework etabliert. Mit bis zu 77.000 Requests pro Sekunde (vs. ~15.000 bei Express), nativem TypeScript-Support und einem integrierten Schema-System auf Basis von JSONSchema bietet Fastify alles, was moderne Backend-Teams benötigen. Claude Code versteht Fastifys Plugin-Architektur, Scope-Kapselung und Lifecycle-Hooks tief — und hilft dabei, typsichere, gut dokumentierte APIs in Minuten zu scaffolden.
Express ist flexibel, aber untypisiert und langsam. Fastify erzwingt Schema-First-Design, bietet automatisches Request/Response-Typing via TypeScript-Generics und serialisiert JSON mit fast-json-stringify — 2–3× schneller als JSON.stringify(). Claude Code nutzt diese Strukturen, um konsistenten, wartbaren API-Code zu generieren.
1. Fastify Setup — fastify(), listen(), TypeScript-Integration
Das Setup einer neuen Fastify-Applikation beginnt mit der Installation der Kernpakete und einer durchdachten Verzeichnisstruktur. Claude Code legt bei neuen Projekten immer TypeScript-First an — mit strikten Compiler-Optionen und expliziten Typen für alle Route-Handler.
Terminal — Installation# Fastify + TypeScript-Toolchain
npm install fastify @fastify/type-provider-typebox
npm install -D typescript @types/node ts-node nodemon
# Optionale Plugins vorab installieren
npm install @fastify/swagger @fastify/swagger-ui
npm install @fastify/cors @fastify/helmet @fastify/jwt
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
src/app.ts — Applikations-Einstiegspunkt
import Fastify, { FastifyInstance } from 'fastify'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
export async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined
},
trustProxy: true
}).withTypeProvider<TypeBoxTypeProvider>()
// Plugins registrieren
await app.register(import('./plugins/cors'))
await app.register(import('./plugins/auth'))
await app.register(import('./routes/users'), { prefix: '/api/v1' })
await app.register(import('./routes/products'), { prefix: '/api/v1' })
return app
}
async function main() {
const app = await buildApp()
try {
await app.listen({
port: Number(process.env.PORT) || 3000,
host: '0.0.0.0'
})
} catch (err) {
app.log.error(err)
process.exit(1)
}
}
main()
fastify-plugin (fp) — Warum es wichtig ist
Fastify kapselt Plugins standardmäßig in einen eigenen Scope. Dekoratoren, die in einem Plugin registriert werden, sind im Eltern-Scope nicht sichtbar — außer das Plugin wird mit fastify-plugin (kurz fp) gewrappt. Claude Code erkennt dieses Muster und wendet es automatisch an, wenn ein Plugin app-weit verfügbar sein soll.
import fp from 'fastify-plugin'
import { FastifyPluginAsync } from 'fastify'
import { Pool } from 'pg'
declare module 'fastify' {
interface FastifyInstance {
db: Pool
}
}
const dbPlugin: FastifyPluginAsync = async (app) => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
// Dekorator macht pool app-weit verfügbar
app.decorate('db', pool)
app.addHook('onClose', async () => {
await pool.end()
})
}
// fp() hebt Scope-Kapselung auf → db ist überall erreichbar
export default fp(dbPlugin, { name: 'db' })
Frage Claude Code: "Erstelle ein Fastify-Projekt mit TypeScript, PostgreSQL-Plugin (fp) und JWT-Auth — strukturiert nach Clean Architecture". Claude erzeugt die vollständige Verzeichnisstruktur, tsconfig, Plugin-Registrierung und Type-Augmentation in einem Durchlauf.
2. Routes und Schema — route(), JSONSchema-Validierung
Fastifys größter Vorteil gegenüber Express ist das integrierte Schema-System. Jede Route kann ein JSONSchema für body, querystring, params und response definieren. Fastify validiert eingehende Daten automatisch und serialisiert Antworten schneller durch das Schema.
import { FastifyPluginAsync } from 'fastify'
import { Type } from '@sinclair/typebox'
// TypeBox-Schemas (kompatibel mit JSONSchema + TypeScript-Types)
const UserParams = Type.Object({
id: Type.String({ format: 'uuid' })
})
const CreateUserBody = Type.Object({
name: Type.String({ minLength: 2, maxLength: 100 }),
email: Type.String({ format: 'email' }),
role: Type.Union([Type.Literal('admin'), Type.Literal('user')])
})
const UserResponse = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String(),
role: Type.String(),
createdAt: Type.String({ format: 'date-time' })
})
const ListQuery = Type.Object({
page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 }))
})
const usersRoutes: FastifyPluginAsync = async (app) => {
// GET /api/v1/users — Alle User auflisten
app.get('/users', {
schema: {
tags: ['users'],
summary: 'Alle User auflisten',
querystring: ListQuery,
response: {
200: Type.Object({
data: Type.Array(UserResponse),
total: Type.Integer(),
page: Type.Integer()
})
}
}
}, async (request, reply) => {
const { page = 1, limit = 20 } = request.query
const offset = (page - 1) * limit
const { rows, rowCount } = await app.db.query(
'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
[limit, offset]
)
return { data: rows, total: rowCount ?? 0, page }
})
// POST /api/v1/users — Neuen User anlegen
app.post('/users', {
schema: {
tags: ['users'],
summary: 'Neuen User anlegen',
body: CreateUserBody,
response: {
201: UserResponse,
409: Type.Object({ error: Type.String() })
}
}
}, async (request, reply) => {
const { name, email, role } = request.body
const existing = await app.db.query('SELECT id FROM users WHERE email = $1', [email])
if (existing.rowCount) {
return reply.code(409).send({ error: 'E-Mail bereits registriert' })
}
const { rows: [user] } = await app.db.query(
'INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING *',
[name, email, role]
)
return reply.code(201).send(user)
})
// GET /api/v1/users/:id
app.get('/users/:id', {
schema: {
tags: ['users'],
params: UserParams,
response: {
200: UserResponse,
404: Type.Object({ error: Type.String() })
}
}
}, async (request, reply) => {
const { rows: [user] } = await app.db.query(
'SELECT * FROM users WHERE id = $1', [request.params.id]
)
if (!user) return reply.code(404).send({ error: 'User nicht gefunden' })
return user
})
}
export default usersRoutes
Fastify verwendet ajv intern für Validation. Bei fehlerhaften Requests antwortet Fastify automatisch mit 400 Bad Request und einer strukturierten Fehlermeldung — ohne zusätzlichen Code. Das response-Schema aktiviert fast-json-stringify für die Antwort-Serialisierung.
3. Plugins und Dekoratoren — Encapsulation & Dependency Injection
Das Plugin-System ist Fastifys architektonisches Herzstück. Plugins sind isolierte Einheiten mit eigenem Scope — sie können Dekoratoren, Routes und Hooks registrieren, ohne den globalen State zu verschmutzen. Dieses Modell ermöglicht echte Dependency Injection in Node.js.
fastify.decorate() — Eigene Properties hinzufügen
decorate() fügt dem Fastify-Instanz-Objekt eigene Properties oder Methoden hinzu. Mit decorateRequest() und decorateReply() lassen sich Request- und Reply-Objekte erweitern.
import fp from 'fastify-plugin'
import jwt from '@fastify/jwt'
import { FastifyPluginAsync, FastifyRequest } from 'fastify'
interface JwtPayload {
sub: string
email: string
role: 'admin' | 'user'
}
declare module 'fastify' {
interface FastifyInstance {
authenticate(req: FastifyRequest): Promise<void>
adminOnly(req: FastifyRequest): Promise<void>
}
interface FastifyRequest {
user: JwtPayload
}
}
const authPlugin: FastifyPluginAsync = async (app) => {
await app.register(jwt, {
secret: process.env.JWT_SECRET!,
sign: { expiresIn: '7d' }
})
// Dekorator: Auth-Middleware als Methode
app.decorate('authenticate', async (req: FastifyRequest) => {
await req.jwtVerify<JwtPayload>()
})
app.decorate('adminOnly', async (req: FastifyRequest) => {
await req.jwtVerify<JwtPayload>()
if (req.user.role !== 'admin') {
throw app.httpErrors.forbidden('Nur Admins erlaubt')
}
})
}
export default fp(authPlugin, { name: 'auth', dependencies: [] })
Route mit Auth-Dekorator als preHandler
app.get('/admin/stats', {
schema: { tags: ['admin'], security: [{ bearerAuth: [] }] },
preHandler: [app.adminOnly] // Nur Admins können diese Route aufrufen
}, async (request) => {
return { userId: request.user.sub, role: request.user.role }
})
Scope-Kapselung verstehen
| Mit fp() | Ohne fp() |
|---|---|
| Dekoratoren app-weit sichtbar | Dekoratoren nur im eigenen Scope |
| Hooks gelten für alle Routes | Hooks nur für Routes im Scope |
| Ideal für: DB, Auth, Logger | Ideal für: Route-Gruppen, Feature-Module |
| Kein automatisches Cleanup | onClose innerhalb des Scopes |
Gib Claude Code: "Erstelle ein Fastify-Plugin für Rate-Limiting mit Redis und fp() — 100 Requests/Min per IP, Whitelist für interne IPs, TypeScript-Typen vollständig". Claude kennt das Plugin-Pattern und generiert sofort produktionsreifen Code mit Type-Augmentation.
4. Hooks — Lifecycle und Request-Pipeline
Fastify bietet einen vollständigen Request/Response-Lifecycle mit präzise definierten Hook-Punkten. Mit addHook() lässt sich in jeden Schritt eingreifen — von der Authentifizierung über die Serialisierung bis hin zum Error-Handling.
Lifecycle-Diagramm
Fehlerfall: onError → onSend → onResponse
| Hook | Zeitpunkt | Typischer Einsatz |
|---|---|---|
onRequest | Vor Parsing | Rate-Limiting, IP-Filter, Logging |
preParsing | Vor Body-Parsing | Content-Decryption, Streaming |
preValidation | Vor Schema-Validation | Body-Transformation |
preHandler | Vor Handler | Auth, Permissions, Cache-Check |
onSend | Vor Antwort-Senden | Response-Compression, Header-Injection |
onResponse | Nach Antwort | Access-Log, Metrics, Analytics |
onError | Bei Fehler | Error-Transformation, Sentry-Integration |
onClose | App-Shutdown | DB-Verbindung schließen, Cleanup |
import fp from 'fastify-plugin'
import { FastifyPluginAsync } from 'fastify'
const observability: FastifyPluginAsync = async (app) => {
// Request-ID und Start-Zeit setzen
app.addHook('onRequest', async (request) => {
request.log.info({
method: request.method,
url: request.url,
ip: request.ip
}, 'incoming request')
})
// Response-Timing zum Header hinzufügen
app.addHook('onSend', async (request, reply, payload) => {
reply.header('X-Response-Time', `${reply.getResponseTime().toFixed(2)}ms`)
reply.header('X-Request-Id', request.id)
return payload // payload IMMER zurückgeben!
})
// Access-Log nach der Antwort
app.addHook('onResponse', async (request, reply) => {
request.log.info({
statusCode: reply.statusCode,
responseTime: reply.getResponseTime(),
url: request.url
}, 'request completed')
})
// Globales Error-Handling
app.addHook('onError', async (request, reply, error) => {
if (error.statusCode && error.statusCode < 500) return
// Unerwartete Fehler an Sentry/Datadog senden
request.log.error({ err: error, url: request.url }, 'unhandled error')
})
}
export default fp(observability)
Route-spezifischer preHandler Hook
// preHandler direkt an einer Route (überschreibt nicht globale Hooks)
app.get('/protected', {
preHandler: async (request, reply) => {
const apiKey = request.headers['x-api-key']
if (apiKey !== process.env.INTERNAL_API_KEY) {
return reply.code(401).send({ error: 'Unauthorized' })
}
}
}, async () => ({ status: 'ok' }))
Im onSend-Hook MUSS das payload-Argument zurückgegeben werden — auch wenn es nicht verändert wurde. Gibt der Hook undefined zurück, sendet Fastify eine leere Antwort.
5. Swagger/OpenAPI — Auto-Dokumentation aus Schema
Da Fastify-Routes sowieso JSONSchema-Definitionen erfordern, entsteht die API-Dokumentation ohne Extra-Aufwand: @fastify/swagger liest die Schemas und generiert daraus vollständige OpenAPI 3.0-Definitionen. @fastify/swagger-ui stellt eine interaktive UI bereit.
import fp from 'fastify-plugin'
import swagger from '@fastify/swagger'
import swaggerUi from '@fastify/swagger-ui'
import { FastifyPluginAsync } from 'fastify'
const swaggerPlugin: FastifyPluginAsync = async (app) => {
await app.register(swagger, {
openapi: {
openapi: '3.0.3',
info: {
title: 'My Fastify API',
description: 'Typsichere REST API mit Fastify 2026',
version: '1.0.0'
},
servers: [
{ url: 'https://api.example.com', description: 'Produktion' },
{ url: 'http://localhost:3000', description: 'Entwicklung' }
],
tags: [
{ name: 'users', description: 'User-Management' },
{ name: 'products', description: 'Produkt-Verwaltung' },
{ name: 'admin', description: 'Admin-Endpunkte (geschützt)' }
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
}
})
await app.register(swaggerUi, {
routePrefix: '/docs',
uiConfig: {
docExpansion: 'tag',
deepLinking: true,
tryItOutEnabled: true
},
staticCSP: true,
transformStaticCSP: (header) => header
})
}
export default fp(swaggerPlugin, { name: 'swagger' })
Tags und Security in Routes
Das schema-Objekt jeder Route kann tags, summary, description und security enthalten — diese Felder fließen direkt in die OpenAPI-Dokumentation ein.
app.post('/products', {
schema: {
tags: ['products'],
summary: 'Neues Produkt erstellen',
description: 'Legt ein neues Produkt an. Erfordert Admin-Rechte.',
security: [{ bearerAuth: [] }],
body: CreateProductBody, // TypeBox-Schema → automatisch in OpenAPI
response: {
201: ProductResponse,
400: Type.Object({
statusCode: Type.Integer(),
error: Type.String(),
message: Type.String()
})
}
},
preHandler: [app.adminOnly]
}, async (req, reply) => {
// Handler-Implementierung
return reply.code(201).send(createdProduct)
})
Nach dem Start ist die Swagger-UI unter /docs erreichbar — mit "Try it out"-Funktion, JWT-Auth-Formular und vollständig generierten Request/Response-Beispielen. Kein manuelles Pflegen von Swagger-YAML mehr.
Claude Code kann aus einer bestehenden OpenAPI-YAML-Datei vollständige Fastify-Routes generieren — inklusive TypeBox-Schemas, Handler-Stubs und Unit-Tests. Prompt: "Generiere Fastify-Routes aus dieser openapi.yaml".
6. Performance und Serialisierung — Benchmarks vs. Express
Fastifys Performance-Vorsprung kommt aus drei Quellen: fast-json-stringify für schnelle Response-Serialisierung, pino als ultra-schneller JSON-Logger und einem optimierten Router-Algorithmus (Radix-Tree). Mit den richtigen Einstellungen erreichst du in Produktion Throughput, der Express um den Faktor 4–5 übertrifft.
fast-json-stringify — Wie es funktioniert
Wenn ein response-Schema definiert ist, kompiliert Fastify beim Start eine spezialisierte Serialisierungs-Funktion für genau dieses Schema. Zur Laufzeit wird kein allgemeines JSON.stringify() mehr aufgerufen — stattdessen direkt die optimierte Funktion.
import build from 'fast-json-stringify'
const stringify = build({
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
price: { type: 'number' },
stock: { type: 'integer' }
}
})
// Bis zu 3x schneller als JSON.stringify für große Arrays
const json = stringify({ id: 'abc', name: 'Widget', price: 9.99, stock: 42 })
pino Logger — Zero-Overhead Logging
Pino-Konfiguration für Produktionconst app = Fastify({
logger: {
level: 'info',
// In Produktion: JSON-Logs direkt an stdout (kein Pretty-Print)
serializers: {
req(request) {
return {
method: request.method,
url: request.url,
hostname: request.hostname
}
}
},
// Redact sensible Felder aus Logs
redact: ['req.headers.authorization', '*.password', '*.token']
}
})
undici — HTTP-Client für externe APIs
Für ausgehende HTTP-Requests innerhalb der API empfiehlt sich undici — der native Node.js HTTP-Client mit Connection-Pooling und bis zu 5× höherem Throughput als axios.
import fp from 'fastify-plugin'
import { Pool } from 'undici'
declare module 'fastify' {
interface FastifyInstance { httpPool: Pool }
}
export default fp(async (app) => {
const pool = new Pool('https://external-api.example.com', {
connections: 10,
pipelining: 4
})
app.decorate('httpPool', pool)
app.addHook('onClose', async () => pool.close())
})
Benchmark-Vergleich 2026
| Framework | Req/s (einfache Route) | Latenz p99 | JSON-Serialisierung |
|---|---|---|---|
| Fastify 5.x | 77.200 | 1.8ms | fast-json-stringify |
| Hono (Bun) | 62.400 | 2.1ms | native |
| Elysia (Bun) | 58.000 | 2.3ms | native |
| Express 5.x | 14.800 | 8.9ms | JSON.stringify |
| NestJS (Express) | 12.300 | 11.2ms | JSON.stringify |
| NestJS (Fastify) | 48.000 | 3.2ms | fast-json-stringify |
1. Response-Schema für jede Route definieren (aktiviert fast-json-stringify) — 2. logger: false nur in Tests, in Produktion pino nutzen — 3. trustProxy: true hinter Reverse-Proxy (nginx/Caddy) — 4. return reply.send() nicht await reply.send() — 5. undici statt axios für ausgehende Requests.
import Fastify from 'fastify'
import { Type } from '@sinclair/typebox'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
const app = Fastify({ logger: true, trustProxy: true })
.withTypeProvider<TypeBoxTypeProvider>()
app.get('/health', {
schema: {
response: {
200: Type.Object({
status: Type.Literal('ok'),
version: Type.String(),
uptime: Type.Number()
})
}
}
}, () => ({
status: 'ok',
version: process.env.npm_package_version ?? '1.0.0',
uptime: process.uptime()
}))
await app.listen({ port: 3000, host: '0.0.0.0' })
Fastify APIs mit Claude Code bauen
Starte deinen kostenlosen Trial — Claude Code scaffoldet vollständige Fastify-Projekte mit TypeScript, Swagger, Auth-Plugins und Tests in Minuten.
Kostenlos testen — kein Kreditkarte nötig