So erstellen Sie eine REST API mit Fastify
Erstelle eine Fastify-v5-REST-API mit Prisma, JSON-Schema-Validierung, Plugins, Decorators, Hooks und TypeBox-Typisierung für CRUD-Posts.
Fastify ist ein hochperformantes Node.js-Webframework mit geringem Overhead, dessen charakteristisches Merkmal die JSON-Schema-gesteuerte Validierung und Serialisierung ist: Sie deklarieren die Struktur jeder Anfrage und Antwort, und Fastify kompiliert diese Schemas beim Start in schnelle Funktionen. Nach Kenntnis der Maintainer ist Fastify eines der schnellsten Webframeworks auf dem Markt und verarbeitet je nach Code-Komplexität bis zu 76.000 und mehr Anfragen pro Sekunde. Dieses Tutorial erstellt eine vollständige CRUD-REST-API für eine posts-Ressource mit Fastify v5 und Prisma als Datenschicht und konzentriert sich auf die vier Grundprinzipien, die Fastify von Express unterscheiden: gekapselte Plugins, Dekoratoren, schemabasierte Validierung/Serialisierung und Lifecycle-Hooks.
Wichtige Erkenntnisse
- Fastify v5 erfordert Node.js v20 oder neuer; v4 hat am 30. Juni 2025 sein End-of-Life erreicht, und v3 sowie ältere Versionen werden nicht mehr gepflegt, daher sollten neue APIs auf der v5.x-Linie starten.
- Ein Fastify-
response-Schema erfüllt eine doppelte Funktion: Es beschleunigt die Serialisierung durch die Kompilierung eines dedizierten Stringifiers und filtert alle Eigenschaften heraus, die nicht im Schema deklariert sind, sodass interne Felder niemals in Antworten durchsickern. - Ab v5 ist die Schema-Kurzschreibweise entfernt: Jedes
querystring-,params-,body- undresponse-Schema muss ein vollständiges JSON-Schema sein, das einetype-Eigenschaft enthält. - Teilen Sie einen Datenbank-Client über Routen hinweg, indem Sie das Plugin in
fastify-plugineinwickeln, um die Kapselung gezielt aufzuheben, und den Client anschließend mitfastify.decorate('prisma', client)anhängen. - Mit dem TypeBox-Type-Provider deklarieren Sie jedes Schema einmal, und Fastify leitet daraus die Request- und Response-Typen ab, wodurch Abweichungen zwischen Validierungsregeln und TypeScript-Typen vermieden werden.
Warum Fastify: die vier Grundprinzipien, die Express nicht hat
Sie können dieselben CRUD-Endpunkte in Express schreiben. Was Sie jedoch nicht ohne Weiteres nachbilden können, ist Fastifys Architekturmodell, das auf vier Grundprinzipien aufbaut: gekapselte Plugins, Dekoratoren, schemabasierte Validierung und Serialisierung sowie Lifecycle-Hooks. Die Maintainer betonen, dass Fastify vollständig über seine Hooks, Plugins und Dekoratoren erweiterbar ist. Diese sind keine nachträglich an einen Router angehängten Hilfsmittel, sondern bilden die eigentliche Architektur.
| Aspekt | Express (typisch) | Fastify (idiomatisch) |
|---|---|---|
| Eingabevalidierung | Manuell oder per Middleware (express-validator) | JSON-Schema direkt an der Route deklariert |
| Response-Serialisierung | JSON.stringify | Kompiliertes fast-json-stringify aus einem Response-Schema |
| Ressourcenteilung | Globale Middleware auf einem gemeinsamen req | Gekapselte Plugins + Dekoratoren |
| Request-Lifecycle | Lineare Middleware-Kette | Typisierte Hooks (onRequest, preHandler, onError, …) |
Der Performance-Vorteil ist technisch begründet, kein Marketing. Obwohl es nicht zwingend erforderlich ist, empfiehlt Fastify die Verwendung von JSON-Schema zur Routenvalidierung und Ausgabeserialiserung; intern kompiliert Fastify das Schema in eine hochperformante Funktion. Ein Response-Schema wird beim Start in einen spezialisierten Serializer umgewandelt, sodass die JSON-Erzeugung den generischen JSON.stringify-Pfad umgeht. Betrachten Sie die offiziellen Zahlen für das, was sie sind: Diese Benchmarks werden mit einem synthetischen „Hello World”-Benchmark ermittelt, der den Framework-Overhead messen soll — benchmarken Sie Ihre eigene Anwendung, bevor Sie Zahlen zitieren.
Projekteinrichtung und ein minimaler v5-Server
Discover how at OpenReplay.com.
Beginnen Sie mit ESM, da sowohl Fastify als auch Prisma v7 ESM-first sind. Fastify v5 unterstützt nur Node.js v20+; wenn Sie eine ältere Node.js-Version verwenden, müssen Sie auf eine neuere Version upgraden, um Fastify v5 nutzen zu können.
mkdir fastify-posts-api && cd fastify-posts-api
npm init -y
npm pkg set type="module"
npm i fastify@5
Das aktuelle Stable-Release ist 5.9.0, veröffentlicht am 28. Juni 2026. Erstellen Sie server.js:
import Fastify from 'fastify'
const app = Fastify({ logger: true })
app.get('/health', async () => ({ status: 'ok' }))
try {
await app.listen({ port: 3000 })
} catch (err) {
app.log.error(err)
process.exit(1)
}
Beachten Sie den Aufruf von listen mit einem Objekt als Argument. Die variadic-Argument-Signatur der .listen()-Methode wurde in v5 entfernt, sodass Sie .listen() nicht mehr mit einer variablen Anzahl von Argumenten aufrufen können — übergeben Sie stets ein Options-Objekt wie { port: 3000 }. Starten Sie den Server mit node --watch server.js und rufen Sie http://localhost:3000/health auf.
Routing auf Fastify-Art: Plugins und Kapselung
In Fastify leben Routen innerhalb von Plugins, und jedes Plugin läuft in seinem eigenen Kapselungskontext. Ein Dekorator, Hook oder eine Route, die innerhalb eines Plugins registriert wird, ist für dieses Plugin und seine Kinder sichtbar, aber für seine Geschwister unsichtbar — das Gegenteil von Express-Middleware, bei der alles ein gemeinsames globales Request-Objekt teilt. Die Plugins-Referenz dokumentiert dieses Scoping-Modell.
Ein Plugin ist lediglich eine async-Funktion, die die gekapselte Instanz empfängt:
// routes/posts.js
export default async function postsRoutes(app, opts) {
app.get('/', async (request, reply) => {
return [] // List-Handler folgt später
})
}
Registrieren Sie es unter einem Präfix in server.js:
import postsRoutes from './routes/posts.js'
app.register(postsRoutes, { prefix: '/api/posts' })
Halten Sie Plugin-Funktionen konsequent als async. Der v5-Migrationsleitfaden stellt ausdrücklich klar, dass alle in v4 als veraltet markierten Funktionen entfernt wurden und nach dem Upgrade nicht mehr funktionieren. Das Mischen von Callback- und Promise-Stil innerhalb eines Plugins — in v4 noch toleriert — ist nicht länger erlaubt. Entscheiden Sie sich für async/await und bleiben Sie dabei.
Schema-Validierung und Serialisierung
Hängen Sie ein schema-Objekt an eine beliebige Route, und Fastify validiert den eingehenden body, params, querystring und headers, serialisiert dann die ausgehende Payload gegen das response-Schema. Dies ist das Markenzeichen des Frameworks, dokumentiert unter Validation and Serialization.
Eine v5-Regel gilt für jedes Schema, das Sie schreiben: Die Kurzschreibweise ist entfernt. Ab v5 müssen Sie für querystring, params, body und response ein vollständiges JSON-Schema angeben, das jeweils die type-Eigenschaft enthält, sonst wird die Route nicht validiert — der v5-Migrationsleitfaden dokumentiert die Entfernung der jsonShortHand-Option.
Definieren Sie die Schemas für die posts-Ressource:
// schemas/posts.js
export const postResponse = {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
content: { type: 'string' },
published: { type: 'boolean' },
createdAt: { type: 'string' }
}
}
export const createPostBody = {
type: 'object',
required: ['title', 'content'],
properties: {
title: { type: 'string', minLength: 1 },
content: { type: 'string', minLength: 1 },
published: { type: 'boolean', default: false }
}
}
export const idParams = {
type: 'object',
required: ['id'],
properties: { id: { type: 'string' } }
}
Das response-Schema ist der Teil, den die meisten Tutorials überspringen, und es rechtfertigt seinen Platz gleich doppelt. Es wird zu einem dedizierten Serializer kompiliert, der schneller ist als die generische Stringifizierung, und es fungiert als Allowlist: Jede Eigenschaft, die Ihr Handler zurückgibt und die nicht im Schema deklariert ist, wird herausgefiltert, bevor sie den Client erreicht. Ein passwordHash oder eine interne userId, die sich in ein Abfrageergebnis einschleicht, verlässt den Server niemals, da der Serializer schlicht nicht weiß, wie er sie ausgeben soll.
Der TypeScript-Weg mit TypeBox
In TypeScript besteht die Herausforderung darin, Validierungsschemas und Typdefinitionen synchron zu halten. Der TypeBox-Type-Provider fasst beides in einer einzigen Deklaration zusammen. Installieren Sie typebox als erforderliche Peer-Dependency und @fastify/type-provider-typebox:
npm i typebox @fastify/type-provider-typebox
Registrieren Sie den Provider und deklarieren Sie jedes Schema einmal mit Type:
import Fastify from 'fastify'
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()
app.post('/api/posts', {
schema: {
body: Type.Object({
title: Type.String({ minLength: 1 }),
content: Type.String({ minLength: 1 })
})
}
}, async (request) => {
// request.body ist typisiert als { title: string; content: string }
const { title, content } = request.body
return { title, content }
})
Sie importieren Type und TypeBoxTypeProvider aus @fastify/type-provider-typebox und rufen Fastify().withTypeProvider<TypeBoxTypeProvider>() auf; die Feldtypen des Body werden dann automatisch abgeleitet. Verwenden Sie das Scoped-Package — der in älteren Anleitungen gezeigte Legacy-Import @sinclair/typebox ist der Name vor v5. Eine v5-Besonderheit, die es zu beachten gilt: Der Migrationsleitfaden weist darauf hin, dass die Type-Provider in zwei separate Typen aufgeteilt wurden: ValidatorSchema und SerializerSchema — aktualisieren Sie daher das Provider-Package zusammen mit Fastify. Beachten Sie außerdem, dass die Provider-Typen nicht global propagiert werden; bei gekapselter Verwendung muss der Kontext auf einen oder mehrere Provider umgemappt werden, was bedeutet, dass Sie den Provider in jedem Plugin neu deklarieren müssen, das ihn benötigt.
Die Datenbankschicht als Dekorator-Plugin
Binden Sie die Datenbank über einen Dekorator an die Fastify-Instanz, bereitgestellt durch ein Plugin, das die Kapselung gezielt aufhebt. Um eine Ressource — einen Datenbank-Client — über alle Routen hinweg zu teilen, wickeln Sie das Plugin in fastify-plugin ein, damit die Dekoration ihren eigenen Geltungsbereich verlässt, und hängen Sie sie dann mit fastify.decorate('prisma', client) an. Die Decorators-Referenz beschreibt dieses Muster.
Dieses Tutorial verwendet Prisma (v7, aktuell 7.8.x), das Rust-frei und ausschließlich ESM-basiert ist — eine saubere Ergänzung zum obigen ESM-Setup. Installieren Sie es, definieren Sie das Schema und generieren Sie den Client. Prisma 7 erfordert außerdem einen Driver-Adapter zur Client-Erstellung, installieren Sie daher den SQLite-Adapter zusammen mit den Core-Packages:
npm i @prisma/client@7 @prisma/adapter-better-sqlite3
npm i -D prisma@7
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Post {
id String @id @default(uuid())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
}
Prisma 7 konfiguriert seine CLI über eine prisma.config.ts-Datei und generiert den Client in den oben festgelegten output-Pfad; führen Sie npx prisma migrate dev aus, um die Datenbank und den Client zu erstellen. Stellen Sie ihn dann als Dekorator bereit. Beachten Sie, dass in Prisma 7 der einfache new PrismaClient()-Aufruf entfernt wurde — Sie müssen den Client mit einem Driver-Adapter konstruieren:
// plugins/prisma.js
import fp from 'fastify-plugin'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { PrismaClient } from '../generated/prisma/client.js'
export default fp(async (app) => {
const adapter = new PrismaBetterSqlite3({ url: 'file:./dev.db' })
const prisma = new PrismaClient({ adapter })
await prisma.$connect()
app.decorate('prisma', prisma)
app.addHook('onClose', async (instance) => {
await instance.prisma.$disconnect()
})
})
Da das Plugin in fp eingewickelt ist, ist app.prisma nun von jeder Route der Anwendung aus erreichbar.
Bevorzugen Sie Postgres mit TypeORM? Das Dekorator-Muster ist identisch: Verbinden Sie die DataSource, dann app.decorate('db', dataSource). Nur die Schema-Schicht ändert sich.
CRUD-Handler mit korrekten Statuscodes
Implementieren Sie die fünf Operationen als Routen innerhalb des Posts-Plugins, jede mit ihrem Schema. Geben Sie den richtigen Statuscode für jedes Verb zurück:
201mit der erstellten Ressource bei POST;200bei Lesezugriffen;204ohne Body bei DELETE; und404, wenn eine Suche kein Ergebnis liefert.
Fastify setzt Content-Type und serialisiert automatisch gegen Ihr Response-Schema — siehe die Reply-API.
// routes/posts.js
import { postResponse, createPostBody, idParams } from '../schemas/posts.js'
export default async function postsRoutes(app) {
// CREATE
app.post('/', {
schema: { body: createPostBody, response: { 201: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.create({ data: request.body })
return reply.code(201).send(post)
})
// READ all
app.get('/', {
schema: { response: { 200: { type: 'array', items: postResponse } } }
}, async () => {
return app.prisma.post.findMany({ orderBy: { createdAt: 'desc' } })
})
// READ one
app.get('/:id', {
schema: { params: idParams, response: { 200: postResponse } }
}, async (request, reply) => {
const post = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!post) return reply.code(404).send({ message: 'Post not found' })
return post
})
// UPDATE
app.put('/:id', {
schema: { params: idParams, body: createPostBody, response: { 200: postResponse } }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
return app.prisma.post.update({ where: { id: request.params.id }, data: request.body })
})
// DELETE
app.delete('/:id', {
schema: { params: idParams }
}, async (request, reply) => {
const exists = await app.prisma.post.findUnique({ where: { id: request.params.id } })
if (!exists) return reply.code(404).send({ message: 'Post not found' })
await app.prisma.post.delete({ where: { id: request.params.id } })
return reply.code(204).send()
})
}
Eine v5-Besonderheit, die beim DELETE zu beachten ist: In v4 erlaubte Fastify DELETE-Anfragen mit einem Content-Type: application/json-Header und einem leeren Body; dies ist in v5 nicht mehr erlaubt. Senden Sie keinen Content-Type-Header, wenn kein Payload vorhanden ist.
Fehlerbehandlung, Hooks und Produktionshärtung
Zentralisieren Sie die Fehlerbehandlung mit setErrorHandler und greifen Sie mit Hooks in den Request-Lifecycle ein — die beiden Erweiterungspunkte, die die Middleware-Kette von Express ersetzen. Die Hooks-Referenz listet den Request-Ablauf der Reihe nach auf: onRequest → preParsing → preValidation → preHandler → Handler → preSerialization → onSend → onResponse, wobei onError ausgelöst wird, wenn etwas einen Fehler wirft.
class NotFoundError extends Error {
constructor (message) { super(message); this.statusCode = 404 }
}
app.setErrorHandler((error, request, reply) => {
request.log.error(error)
const status = error.statusCode ?? 500
reply.code(status).send({
error: error.name,
message: status === 500 ? 'Internal Server Error' : error.message
})
})
// preHandler-Hook-Beispiel: Route vor dem Handler absichern
app.addHook('preHandler', async (request) => {
request.log.info({ url: request.url }, 'incoming request')
})
Ein Validierungsfehler unterbricht die Verarbeitung, bevor Ihr Handler überhaupt ausgeführt wird, und gibt automatisch einen 400-Fehler zurück — ein Grund, warum Schemas den Handler-Code reduzieren. Für Zeitmessungen innerhalb eines Hooks verwenden Sie reply.elapsedTime; die Methode reply.getResponseTime() wurde in v5 entfernt, und Sie sollten stattdessen reply.elapsedTime verwenden.
Von hier aus besteht die Produktions-Checkliste aus Plugin-Installationen, jeweils als Scoped-@fastify/*-Package, das auf dieselbe Weise wie Ihr Routen-Plugin registriert wird:
@fastify/jwtfür Authentifizierung;@fastify/swaggerzur Generierung von OpenAPI-Dokumentation direkt aus Ihren Routen-Schemas;@fastify/rate-limitfür Throttling; und@fastify/corsfür Cross-Origin-Zugriff.
Für tiefergehende Auth-Flows und Deployment verweisen Sie auf dedizierte Anleitungen — das sind separate Themen. Wenn Sie sehen möchten, wie sich diese Endpunkte unter realem Traffic verhalten, kombinieren Sie die API mit Node.js-Fehlerüberwachung auf dem Frontend, das sie konsumiert.
Sie verfügen nun über eine lauffähige Fastify-v5-CRUD-API, bei der Schemas die Eingabe validieren, die Ausgabe serialisieren und sich selbst dokumentieren, und bei der die Datenbank über einen Dekorator statt über einen globalen Import geteilt wird. Der nächste konkrete Schritt: Fügen Sie @fastify/swagger hinzu, verweisen Sie es auf die bereits geschriebenen Schemas, und beobachten Sie, wie die OpenAPI-Spezifikation sich selbst generiert — ein Beweis dafür, dass in Fastify das Schema die einzige Quelle der Wahrheit ist und kein nachträglicher Gedanke.
Häufig gestellte Fragen
Was ist der Unterschied zwischen fastify.register und fastify.decorate?
Die register-Methode bindet ein Plugin in seinen eigenen Kapselungskontext ein, sodass darin hinzugefügte Routen, Hooks und Dekoratoren auf dieses Plugin und seine Kinder beschränkt bleiben. Die decorate-Methode hängt eine wiederverwendbare Eigenschaft oder Methode direkt an die Fastify-Instanz, den Request oder die Reply an. In der Regel verwenden Sie decorate, um gemeinsam genutzte Ressourcen wie einen Datenbank-Client bereitzustellen, und wickeln dieses Plugin in fastify-plugin ein, damit die Dekoration die Kapselung verlässt und anwendungsweit verfügbar wird.
Ist Fastify schneller als Express, und um wie viel?
Fastifys eigener Benchmark meldet bis zu 76.000 und mehr Anfragen pro Sekunde, aber die Maintainer weisen darauf hin, dass es sich um einen synthetischen 'Hello World'-Test handelt, der den Framework-Overhead misst und nicht den realen Durchsatz. Der technische Vorteil ist konkret: Fastify kompiliert JSON-Schemas beim Start in spezialisierte Validierungs- und Serialisierungsfunktionen, sodass die Response-Erzeugung den generischen JSON.stringify-Overhead umgeht. Benchmarken Sie stets Ihre eigene Anwendung, bevor Sie einen Geschwindigkeitsmultiplikator zitieren.
Warum schlägt mein Fastify-v5-Routen-Schema bei der Validierung fehl, obwohl es in v4 funktioniert hat?
Ab v5 wurden die Schema-Kurzschreibweise und die jsonShortHand-Option entfernt, sodass jedes querystring-, params-, body- und response-Schema ein vollständiges JSON-Schema sein muss, das explizit eine type-Eigenschaft enthält. Ein Schema, das nur Eigenschaften ohne ein top-level type deklariert, wird nicht mehr validiert. Fügen Sie type 'object' zu jedem Schema-Objekt hinzu. Der v5-Migrationsleitfaden dokumentiert dies als bewusste Breaking Change gegenüber dem v4-Kurzschreibweise-Verhalten.
Kann ich Fastify mit TypeScript verwenden, ohne Typen doppelt zu schreiben?
Ja. Installieren Sie typebox als Peer-Dependency sowie das Scoped-Package @fastify/type-provider-typebox, und rufen Sie dann Fastify().withTypeProvider mit dem TypeBoxTypeProvider-Generic auf. Sie deklarieren jedes Schema einmal mit Type, und Fastify leitet daraus die Request- und Response-Typen ab, wodurch Abweichungen zwischen Validierungsregeln und TypeScript-Interfaces vermieden werden. Importieren Sie Type aus @fastify/type-provider-typebox, nicht unter dem Legacy-Namen @sinclair/typebox, der in älteren Anleitungen gezeigt wird, und deklarieren Sie den Provider in jedem Plugin neu, das ihn benötigt.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k