Was ist Fastify?
Fastify ist ein Web-Framework für Node.js, das auf einer einzigen Annahme basiert: Wenn du deine Daten beschreibst, kann das Framework einen größeren Teil der Arbeit übernehmen. Es kombiniert einen extrem schnellen Router mit einem Schema-System, einem scoped Plugin-Modell und einem Lifecycle aus Hooks. Das Ergebnis ist ein Framework, das sowohl schnell als auch konsequent in Bezug auf Sicherheit ist, ohne dir eine bestimmte Ordnerstruktur aufzuzwingen.
Fastify erschien 2016 und erreichte 2018 die stabile Version 1.0. Heute treibt es Produktions-APIs in Unternehmen an, die eine Express-ähnliche Ergonomie mit spürbar besserer Performance und einem echten Validierungskonzept benötigen. Wenn du Express kennst, wird dir der Großteil von Fastify bereits innerhalb eines Nachmittags vertraut vorkommen.
Schema-first: Validierung, die kompiliert wird
Das entscheidende Merkmal ist, dass Schemas ausführbar sind. Wenn Sie ein JSON Schema an eine Route binden, kompiliert Fastify dieses einmal beim Start und verwendet den kompilierten Validator für jede Anfrage. Pro Aufruf findet keine Interpretation statt, weshalb die Validierung nahezu kostenlos ist.
import { Type, type Static } from "@sinclair/typebox";
const CreateUser = Type.Object({
name: Type.String({ minLength: 1, maxLength: 80 }),
email: Type.String({ format: "email" }),
});
type CreateUser = Static<typeof CreateUser>;
app.post("/users", {
schema: { body: CreateUser },
}, async (request) => {
// request.body is validated and typed as CreateUser
return createUser(request.body);
});
Das bringt zwei wesentliche Vorteile. Erstens werden ungültige Anfragen mit einem 400 abgelehnt, noch bevor Ihr Handler aufgerufen wird, sodass die Business-Logik ausschließlich saubere Daten erhält. Zweitens steuert dasselbe Schema die Serialisierung: Fastify kompiliert das Response-Schema in eine fast-json-stringify-Funktion, die dramatisch schneller ist als JSON.stringify und garantiert, dass Sie niemals Felder preisgeben, die Sie nicht explizit deklariert haben.
Routes und Route-Optionen
Eine Route besteht aus einer Methode, einem Pfad und einem Handler, aber der interessante Teil ist das Options-Objekt dazwischen. Hier werden das Schema, der Handler und die route-spezifischen Metadaten definiert.
app.route({
method: "GET",
url: "/health",
config: { public: true },
schema: {
response: {
200: Type.Object({ status: Type.String() }),
},
},
handler: async () => ({ status: "ok" }),
});
Die Kurzschreibweisen-Methoden (app.get, app.post und so weiter) akzeptieren dieselben Optionen als zweites Argument. Route-Parameter werden über request.params, der Query-String über request.query und der geparste Body über request.body übergeben. Da die Strukturen aus dem Schema stammen, kennt TypeScript alle drei.
reply ist die andere Hälfte des Handlers. reply.code(404), reply.header(...) und reply.send(...) orientieren sich an Express, und das Zurückgeben eines Wertes aus einem async-Handler ist eine Kurzschreibweise für reply.send. Fastify liefert zudem Helper wie reply.callNotFound() aus, damit Fehlerpfade konsistent bleiben.
Plugins und Kapselung
Fastify besitzt keine Middleware-Chain im Sinne von Express. Stattdessen ist alles ein Plugin, und jedes Plugin erhält seinen eigenen Scope. Diese eine Regel sorgt dafür, dass große Anwendungen vorhersehbar bleiben.
import fp from "fastify-plugin";
const dbPlugin = fp(async (app) => {
app.decorate("users", createUserRepository(app.log));
}, { name: "db" });
await app.register(dbPlugin);
Ohne fastify-plugin wären Decorators und Hooks, die innerhalb des Plugins hinzugefügt wurden, nur für Routen sichtbar, die im selben Plugin registriert sind. Das ist Kapselung: Ein Feature kann seine eigenen Abhängigkeiten und Konfigurationen besitzen, ohne den Rest der App zu „verschmutzen“. Die Umschließung mit fp entfernt diese Grenze bewusst, wenn Sie gemeinsame Infrastruktur wie eine Datenbank oder einen Logger aufbauen.
Decorators sind der idiomatische Weg, um Funktionalität hinzuzufügen: app.decorate("users", repo) für die Instanz, app.decorateRequest("user", null) für den Request und app.decorateReply für den Reply. Da diese über Module Augmentation typisiert werden, erhalten Sie Autocomplete anstelle von any.
Der Lebenszyklus: Hooks in der richtigen Reihenfolge
Hooks ermöglichen es Ihnen, Code an definierten Punkten auszuführen, ohne Handler umschließen zu müssen. Sie werden in einer festen Reihenfolge ausgeführt:
onRequest— der frühestmögliche Zeitpunkt, ideal für Authentifizierung und Request-IDs.preParsing— bevor der Body gelesen wird, für Kompression oder Größenprüfungen.preValidation— nach dem Parsing, vor der Schema-Validierung.preHandler— nach der Validierung, unmittelbar vor dem Handler.preSerializationundonSend— formen den Payload auf dem Rückweg.onResponseundonError— beobachten den abgeschlossenen Request.
app.addHook("onRequest", async (request) => {
request.start = process.hrtime.bigint();
});
app.addHook("onResponse", async (request, reply) => {
const ms = Number(process.hrtime.bigint() - request.start) / 1e6;
request.log.info({ ms }, "request completed");
});
Hooks sind wie Plugins scoped, sodass ein innerhalb eines Plugins hinzugefügter Hook nur für die Routen in diesem Plugin ausgeführt wird. onClose ist das Gegenstück für den Shutdown; hier geben Sie Datenbank-Pools und Timer frei. Die richtige Reihenfolge zu beherrschen, ist die wichtigste Fähigkeit; die Dokumentation listet diese präzise auf, und sie ändert sich selten.
TypeScript ohne unnötigen Overhead
Fastify ist in TypeScript geschrieben und bietet First-Class-Typunterstützung. Sie typisieren eine Route, indem Sie generische Parameter übergeben, sodass Validierungsfehler bereits zur Kompilierzeit und nicht erst in der Produktion auftreten.
app.get<{
Params: { id: string };
Querystring: { fields?: string };
}>("/users/:id", async (request) => {
const { id } = request.params;
const { fields } = request.query;
return findUser(id, fields);
});
Das wertvollste Pattern sind Type Provider. Mit @fastify/type-provider-typebox wird das Schema selbst zum Typ. Dadurch müssen Sie Interfaces nie doppelt schreiben und verhindern, dass Schema und Typ auseinanderlaufen.
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();
app.post("/users", { schema: { body: CreateUser } }, async (request) => {
return createUser(request.body); // typed as CreateUser
});
Über Module Augmentation erhalten Decorators ihre Typen: Deklarieren Sie die Eigenschaft auf FastifyInstance oder FastifyRequest, und sie ist überall verfügbar. Wenn etwas als unknown markiert ist, ist das meist ein Zeichen dafür, dass ein Schema oder eine Deklaration fehlt.
Logging mit pino, integriert
Fastify wird mit pino als Logger ausgeliefert. Aktivieren Sie diesen mit einer einzigen Option, und jede Anfrage erhält eine strukturierte JSON-Logzeile mit einer ID, Timing-Informationen und Ihren eigenen Feldern.
const app = Fastify({
logger: {
level: "info",
redact: ["req.headers.authorization"],
},
});
app.get("/orders", async (request) => {
request.log.info({ userId: request.user?.id }, "listing orders");
return listOrders(request.user?.id);
});
Da pino verwendet wird, ist die Ausgabe ein durch Zeilenumbrüche getrenntes JSON, das sauber an jeden Log-Aggregator übertragen werden kann, und request.log schließt den Request-Kontext automatisch ein. Nutzen Sie in der Entwicklung pino-pretty für eine lesbare Ausgabe; in der Produktion behalten Sie das JSON bei. Redaction-Regeln sind der sicherste Weg, um zu verhindern, dass Tokens in den Logs landen.
Testen mit app.inject()
Um eine Fastify-App zu testen, müssen Sie keinen Port öffnen. app.inject() leitet eine Anfrage durch den gesamten Stack – einschließlich Routing, Validierung und Hooks – und gibt ein Response-Objekt zurück, gegen das Sie Assertions durchführen können.
const app = buildApp();
await app.ready();
const res = await app.inject({
method: "POST",
url: "/users",
payload: { name: "Ada", email: "[email protected]" },
});
assert.equal(res.statusCode, 201);
assert.equal(res.json().name, "Ada");
await app.close();
Zwei Gewohnheiten machen diesen Prozess angenehm. Erstens: Exportieren Sie eine buildApp()-Factory aus app.ts und rufen Sie listen() nur in server.ts auf, sodass Tests niemals einen Port belegen. Zweitens: Rufen Sie await app.ready() vor dem Injecting auf, was dazu führt, dass Plugins und Schemas vollständig geladen werden. Die Tests laufen schnell, da kein Socket verwendet wird, und sie sind isoliert, da jeder Test seine eigene Instanz erstellt.
Best Practices
- Definiere Request- und Response-Schemas für jede Route und leite die Typen anschließend über einen Type Provider ab.
- Halte die App Factory und den Listener in separaten Dateien, damit Tests in-process bleiben.
- Registriere die Infrastruktur mit
fastify-pluginund den Feature-Code ohne, damit die Scopes aussagekräftig bleiben. - Nutze
onRequestfür die Authentifizierung undpreHandlerfür die Autorisierung, die den geparsten Body benötigt. - Zentralisiere die Fehlerformatierung mit
setErrorHandler, anstatt in jedem Handler Verzweigungen einzubauen. - Logge strukturierte Felder, schwärze Secrets und logge niemals komplette Request-Bodies.
- Validiere die Konfiguration mit
@fastify/envund sorge für ein Fail-Fast beim Startup. - Rufe
app.close()in Tests und beiSIGTERMauf, damit Verbindungen sauber geschlossen werden.
Häufige Fehler
- Ein Plugin ohne
fastify-pluginregistrieren und sich wundern, warum Decorators an anderen Stellen undefined sind. await app.ready()in Tests vergessen, sodass Schemas und Plugins noch nicht geladen sind.JSON.stringifymanuell verwenden, obwohl ein Response-Schema schneller und sicherer serialisieren würde.- Response-Schemas weglassen, was dazu führt, dass beliebige Properties an Clients durchsickern können.
- Globale Hooks hinzufügen, obwohl ein scoped Plugin-Hook unerwartete Auswirkungen auf nicht verwandte Routes vermeiden würde.
- Hooks wie middleware behandeln und erwarten, dass
preHandlervor dem Body-Parsing ausgeführt wird. - Die Struktur des Standard-Error-Handlers ignorieren und API-Clients durch inkonsistente Antworten stören.
Wie geht es weiter?
Fastify ist der natürliche nächste Schritt nach Express, wenn Durchsatz und Validierung eine wichtigere Rolle spielen. Wenn Ihr Team eine meinungsstarke Architektur mit Dependency Injection auf Basis von Fastify sucht, lesen Sie den NestJS-Guide. Für einen ähnlichen Web-Standards-Ansatz auf Edge-Runtimes schauen Sie sich Hono an. Und falls sich der Plugin-Lebenszyklus immer noch zu abstrakt anfühlt, kehren Sie zu den Node.js-Grundlagen zurück, auf denen alles aufbaut.