Eine Express API ist einer der schnellsten Wege, mit Node.js ein Backend zu bauen: Mit einer Handvoll Zeilen startet ein HTTP-Server, beantwortet Anfragen und lässt sich beim Wachsen sauber in Schichten aufteilen. In diesem Artikel bauen wir eine kleine, aber echte REST-API von Grund auf — wir organisieren Routen mit Routern, bündeln gemeinsame Aufgaben in Middleware und schließen mit einer zentralen Fehlerbehandlung ab, die dich von verstreuten try/catch-Blöcken befreit.
Das Projekt aufsetzen
Beginne mit einem leeren Ordner und installiere Node und Express. Node.js 18 oder höher wird empfohlen; diese Versionen unterstützen fetch und moderne JavaScript-Funktionen nativ.
mkdir blog-api && cd blog-api
npm init -y
npm install express
npm install --save-dev nodemon
Füge "type": "module" zu deiner package.json hinzu, um ES-Module zu aktivieren, damit du die import-Syntax verwenden kannst. Definieren wir außerdem ein Skript, das den Server bei einer Dateiänderung während der Entwicklung neu startet:
{
"type": "module",
"scripts": {
"dev": "nodemon server.js",
"start": "node server.js"
}
}
Der erste Server
Der Kern einer Express-Anwendung ist ein app-Objekt. Wir fügen die Middleware express.json() hinzu, um eingehende JSON-Bodys zu parsen, und beginnen mit einer einfachen Health-Check-Route.
// server.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API läuft: http://localhost:${PORT}`);
});
Führe npm run dev aus und besuche http://localhost:3000/health — du solltest eine JSON-Antwort sehen. Das war's; du hast eine API. Machen wir sie nun bereit zum Wachsen.
Routen mit einem Router aufteilen
Jede Route in server.js zu stapeln, funktioniert bei winzigen Projekten, wird aber schnell unlesbar. Das Router-Objekt von Express erlaubt es dir, zusammengehörige Routen in einer eigenen Datei zu gruppieren und unter einem einzigen Präfix einzuhängen. Nehmen wir an, wir verwalten eine Posts-Ressource.
// routes/posts.js
import { Router } from "express";
const router = Router();
const posts = [
{ id: 1, title: "Hallo Welt", body: "Erster Beitrag" }
];
router.get("/", (req, res) => {
res.json(posts);
});
router.get("/:id", (req, res) => {
const post = posts.find(p => p.id === Number(req.params.id));
if (!post) {
return res.status(404).json({ error: "Beitrag nicht gefunden" });
}
res.json(post);
});
router.post("/", (req, res) => {
const { title, body } = req.body;
const post = { id: posts.length + 1, title, body };
posts.push(post);
res.status(201).json(post);
});
export default router;
Hänge diesen Router dann in der Hauptanwendung unter dem Präfix /posts ein:
import postsRouter from "./routes/posts.js";
app.use("/posts", postsRouter);
Jetzt funktionieren GET /posts, GET /posts/1 und POST /posts alle. Jede neue Ressource (Benutzer, Kommentare) bekommt ihre eigene Router-Datei, und server.js bleibt schlank.
Middleware: gemeinsame Aufgaben an einem Ort bündeln
Middleware ist eine Kette von Funktionen, die eine Anfrage durchläuft, bevor sie eine Antwort erreicht. Ihre Signatur ist (req, res, next); wenn sie fertig ist, ruft sie next() auf, um die Kontrolle an die nächste weiterzugeben. Logging, Authentifizierung, Rate Limiting und andere querschnittliche Belange leben hier. Schreiben wir einen einfachen Anfrage-Logger:
// middleware/logger.js
export function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
const ms = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${ms}ms`);
});
next();
}
Füge ihn vor allen Routen hinzu, und jede Anfrage wird automatisch protokolliert:
import { logger } from "./middleware/logger.js";
app.use(logger);
Validierung kann ebenfalls eine Middleware sein. Zum Beispiel ein kleiner Wächter, der den Body vor POST /posts prüft:
export function validatePost(req, res, next) {
const { title } = req.body;
if (!title || title.trim() === "") {
return res.status(400).json({ error: "title ist erforderlich" });
}
next();
}
Hänge ihn dann nur an diese Route: router.post("/", validatePost, handler). Das ist die Stärke von Middleware — wiederkehrende Logik einmal schreiben und überall einstecken.
Zentrale Fehlerbehandlung
Eine der nützlichsten Funktionen von Express ist eine spezielle Fehler-Middleware mit vier Argumenten: (err, req, res, next). Diese Funktion wird nach allen Routen definiert und fängt jeden irgendwo geworfenen Fehler ab. So musst du nicht in jeden Handler ein separates try/catch streuen.
Damit Fehler aus async-Funktionen sie erreichen, ist der sauberste Ansatz ein kleiner Wrapper (Express 5 leitet async-Fehler automatisch weiter, aber in 4.x ist dieser Helfer praktisch):
// utils/asyncHandler.js
export const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
Wir können eine Fehlerklasse definieren und sie bequem innerhalb von asyncHandler werfen:
// utils/ApiError.js
export class ApiError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
Schließlich fügen wir die Fehler-Middleware und einen 404-Auffänger für unbekannte Routen hinzu. Diese müssen immer zuletzt kommen:
// 404 — keine Route passt
app.use((req, res) => {
res.status(404).json({ error: "Ressource nicht gefunden" });
});
// Zentrale Fehlerbehandlung
app.use((err, req, res, next) => {
const status = err.status || 500;
if (status === 500) console.error(err);
res.status(status).json({ error: err.message || "Serverfehler" });
});
Jetzt genügt ein einzelnes throw new ApiError(404, "Beitrag nicht gefunden") in einem Handler; diese eine Stelle erledigt den Rest. In der Produktion ist es gute Praxis, die Meldung bei 500-Fehlern generisch zu halten, wie oben, damit du keine Details an den Client preisgibst.
Die Struktur sauber halten
Mit wachsendem Projekt hält eine einfache Ordnerstruktur alles wartbar:
routes/— eine Router-Datei pro Ressource.controllers/— die eigentliche Geschäftslogik, die eine Route bearbeitet (hält den Router dünn).middleware/— wiederverwendbare Teile wie Logger, Auth, Validierung.utils/— Helfer wieasyncHandlerundApiError.
Durch diese Trennung beantwortet der Router nur „welche URL geht zu welcher Funktion", und die eigentliche Arbeit lebt im Controller. Wenn du eine Datenbank hinzufügst (zum Beispiel PostgreSQL oder MongoDB), skaliert diese Struktur nahezu ohne Änderungen.
Häufige Fragen
Könnte ich nicht einfach das eingebaute http-Modul statt Express verwenden?
Könntest du, aber dann müsstest du Routing, Body-Parsing und die Middleware-Kette von Hand schreiben. Express bietet all das als dünne Schicht; es ist leicht zu lernen und sein Ökosystem ist riesig. Selbst für eine kleine API spart es Zeit.
Warum ist die Reihenfolge der Middleware wichtig?
Express führt Middleware in der Reihenfolge aus, in der sie mit app.use hinzugefügt wurde. Du kannst einen Body nicht validieren, bevor express.json() ihn parst, und die Fehler-Middleware muss nach allen Routen kommen, damit sie deren Fehler abfangen kann.
Sollte ich Express 4 oder 5 verwenden?
Express 5 ist nun stabil und leitet Fehler aus async-Handlern automatisch an die Fehler-Middleware weiter. Für neue Projekte kannst du 5 bevorzugen; in bestehenden 4.x-Projekten ist das obige asyncHandler-Muster eine sichere und verbreitete Lösung.
Möchtest du deine API auf die nächste Stufe bringen? Wenn du Authentifizierung, Datenbankanbindung oder ein produktionsreifes Express-Fundament brauchst, schauen wir es uns gemeinsam an. Nimm Kontakt mit mir auf.