Node.js Framework

Fastify

Fastify ist ein schnelles, schema-first Node.js Web-Framework. JSON Schema kompiliert Ihre Validierung und Serialisierung, und kapselnde Plugins sorgen für eine saubere Struktur in großen Services.

intermediate15 min readUpdated 16. Sept. 2026
server.ts
ts
// server.ts
import Fastify from "fastify";
import { Type } from "@sinclair/typebox";

const app = Fastify({ logger: true });

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

app.get("/users/:id", {
  schema: {
    params: Type.Object({ id: Type.String({ format: "uuid" }) }),
    response: { 200: User },
  },
}, async (request, reply) => {
  const user = await db.users.findById(request.params.id);
  if (!user) return reply.callNotFound();
  return user;
});

await app.listen({ port: 3000, host: "0.0.0.0" });
Veröffentlicht
2016
Läuft auf
Node.js
Stil
Schema-first, plugin-basiert
Kernidee
JSON Schema
Sprache
JavaScript / TypeScript
Version
5.x

Warum es wichtig ist

Warum Fastify beim Durchsatz gewinnt

Durchsatz durch Design

Ein Radix-Tree-Router und kompilierte Schemas ermöglichen es Fastify, weitaus mehr Anfragen pro Sekunde zu verarbeiten als Express, während die API vertraut bleibt.

Kompilierbare Validierung

Ein JSON Schema ist nicht nur Dokumentation. Fastify kompiliert es in einen Validator und einen Serializer, sodass ungültige Eingaben abgelehnt werden, bevor Ihr Handler ausgeführt wird.

Kapselnde Plugins

Jedes Plugin erhält seinen eigenen Scope. Decorators und Hooks bleiben lokal, sofern Sie diese nicht explizit teilen, was verhindert, dass große Services Zustände unkontrolliert leaken.

Das Gesamtbild

Die drei Kernideen hinter Fastify

Definieren Sie die Form Ihrer Daten, kapseln Sie Funktionen in Plugins und fangen Sie Anfragen an klar definierten Lifecycle-Punkten ab.

JSON Schema

Deklarieren

Beschreiben Sie Params, Body, Query und Responses einmalig, und Fastify erzwingt und serialisiert diese für Sie.

Plugins

Kapseln

Ein Plugin ist eine Funktion, die die Instanz dekorieren und Routes innerhalb ihres eigenen Scopes registrieren kann.

Hooks

Abfangen

Lifecycle-Hooks wie onRequest, preHandler und onSend werden an präzisen Punkten jeder Anfrage ausgeführt.

Der vollständige Leitfaden

Fastify: Alles was Sie wissen müssen

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:

  1. onRequest — der frühestmögliche Zeitpunkt, ideal für Authentifizierung und Request-IDs.
  2. preParsing — bevor der Body gelesen wird, für Kompression oder Größenprüfungen.
  3. preValidation — nach dem Parsing, vor der Schema-Validierung.
  4. preHandler — nach der Validierung, unmittelbar vor dem Handler.
  5. preSerialization und onSend — formen den Payload auf dem Rückweg.
  6. onResponse und onError — 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-plugin und den Feature-Code ohne, damit die Scopes aussagekräftig bleiben.
  • Nutze onRequest für die Authentifizierung und preHandler fü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/env und sorge für ein Fail-Fast beim Startup.
  • Rufe app.close() in Tests und bei SIGTERM auf, damit Verbindungen sauber geschlossen werden.

Häufige Fehler

  • Ein Plugin ohne fastify-plugin registrieren 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.stringify manuell 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 preHandler vor 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.

In der Praxis

Routes, Plugins und Hooks

Die vier Bausteine eines Fastify-Services: eine typisierte Route, eine gekapselte Abhängigkeit, ein Lifecycle-Hook und ein Inject-Test.

routes/users.ts
import type { FastifyPluginAsync } from "fastify";
import { Type, type Static } from "@sinclair/typebox";

const User = Type.Object({
  id: Type.String({ format: "uuid" }),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: "email" }),
});

const Params = Type.Object({
  id: Type.String({ format: "uuid" }),
});

export const userRoutes: FastifyPluginAsync = async (app) => {
  app.get<{ Params: Static<typeof Params> }>(
    "/users/:id",
    {
      schema: { params: Params, response: { 200: User } },
    },
    async (request, reply) => {
      const user = await app.users.findById(request.params.id);
      if (!user) return reply.callNotFound();
      return user;
    },
  );
};

Den Vertrag definieren

Ein Schema validiert und serialisiert in einer einzigen Deklaration. Manuelle Prüfungen verteilen dieselben Regeln über jeden Handler und driften mit der Zeit auseinander.

Bevorzugt
app.post("/users", {
  schema: {
    body: Type.Object({
      name: Type.String({ minLength: 1 }),
      email: Type.String({ format: "email" }),
    }),
  },
}, createUser);
Vermeiden
app.post("/users", async (request, reply) => {
  const { name, email } = request.body as any;
  if (typeof name !== "string") {
    return reply.code(400).send({ error: "invalid" });
  }
  // validation grows with every field
});

Eine Abhängigkeit teilen

Nutzen Sie Decorate innerhalb eines Plugins, um die Oberfläche der Instanz explizit zu halten. Das manuelle Anhängen von Eigenschaften an die Instanz ist für den Fastify-Lifecycle unsichtbar.

Bevorzugt
export default fp(async (app) => {
  app.decorate("users", createUserRepository());
});
Vermeiden
// Mutating the instance directly skips decorators,
// typing and the onClose lifecycle.
(app as any).users = createUserRepository();

Abwägungen

Lohnt sich der Aufwand für die Schemas?

Fastify tauscht ein wenig mehr Deklarationsaufwand gegen Geschwindigkeit, Sicherheit und Struktur. Die Kosten fallen nur bei kleinsten Projekten ins Gewicht.

Strengths

  • Schnell mit gutem Grund

    Der Router und die kompilierten Serializer sind tatsächlich schneller, sodass dieselbe Hardware mehr Traffic bewältigt, ohne dass ein Rewrite nötig ist.

  • Validierung und Docs aus einer Quelle

    Das Schema, mit dem Sie validieren, kann auch ein OpenAPI-Dokument und TypeScript-Typen generieren, sodass Verträge synchron bleiben.

  • Skalierbare Struktur

    Die Plugin-Kapselung gibt jedem Feature seinen eigenen Scope. Abhängigkeiten werden einmal registriert und gezielt wiederverwendet.

Trade-offs

  • Schemas brauchen Einarbeitungszeit

    JSON Schema ist ausführlicher als ein Zod-Objekt, und die Fehlermeldungen müssen angepasst werden, bevor sie für API-Konsumenten benutzerfreundlich sind.

  • Das Ökosystem ist kleiner

    Es gibt zwar für fast alles ein Plugin, aber weit weniger Stack-Overflow-Antworten als für Express, weshalb man häufiger in der Dokumentation liest.

  • Strenge kann überraschen

    Response-Schemas entfernen unbekannte Eigenschaften standardmäßig. Das ist ein Feature, wirkt aber wie Datenverlust, bis man die Serialisierung versteht.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, Fastify zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch Fastify — mit Quizzen und echtem Code, den Sie im Browser ausführen können.