Erste REST API mit Claude Code bauen: CRUD, Validierung und Tests
Baue eine Express REST API mit Claude Code: CRUD, Validierung, Fehlerdesign, Tests und lauffähiger Beispielcode.
Beim ersten REST-API-Projekt ist selten Express selbst das Problem. Schwieriger ist die Frage, was eine brauchbare API enthalten muss: klare URLs, passende HTTP-Methoden, Validierung, nachvollziehbare Fehlerantworten und Tests, die den wichtigsten Ablauf belegen.
In diesem Leitfaden baust du mit Claude Code eine kleine Todo-API mit Express. Du legst das Projekt an, implementierst CRUD-Endpunkte, validierst JSON, gibst sinnvolle Statuscodes zurück und prüfst alles mit curl und dem integrierten Node.js-Test-Runner. Der Code läuft mit aktuellem Node.js 22/24 LTS oder neuer.
Wenn Claude Code selbst noch neu für dich ist, lies zuerst den Claude Code Einstieg. Für mehr Testtiefe passen danach Claude Code API Testing und Zod Validierung mit Claude Code.
Die wichtigsten Begriffe
Eine REST API ist ein Stil, um Ressourcen über HTTP zu lesen und zu verändern. In diesem Artikel ist die Ressource ein todo. Einfach gesagt: Eine App ruft eine URL auf, sendet oder erhält JSON und kann anhand des Statuscodes entscheiden, was passiert ist.
| Begriff | Einfache Erklärung | Beispiel |
|---|---|---|
| Endpoint | URL-Einstieg der API | GET /todos |
| Methode | HTTP-Verb für die Aktion | GET, POST, PUT, DELETE |
| Statuscode | Zahl für das Ergebnis | 200, 201, 400, 404 |
| JSON | Leichtes Textformat für Daten | { "title": "Learn REST" } |
| Validierung | Prüfung, ob Eingaben erlaubt sind | Leeren title ablehnen |
| Idempotenz | Wiederholung führt zum gleichen Endzustand | Dasselbe PUT zweimal senden |
Offizielle Referenzen sind MDNs HTTP request methods, MDNs HTTP status codes, Express routing, Express error handling und der Node.js test runner.
Was wir bauen
Die API speichert Todos im Arbeitsspeicher. Nach einem Neustart ist der Anfangszustand wieder da. Genau das hält den Einstieg klein, bevor eine Datenbank dazukommt.
| Aktion | Methode und URL | Erfolgscode |
|---|---|---|
| Healthcheck | GET /health | 200 OK |
| Todos listen | GET /todos | 200 OK |
| Ein Todo lesen | GET /todos/:id | 200 OK |
| Todo erstellen | POST /todos | 201 Created |
| Todo ersetzen | PUT /todos/:id | 200 OK |
| Todo löschen | DELETE /todos/:id | 204 No Content |
Der kleine Umfang ist realitätsnah. Erstens kann daraus eine interne Aufgaben-API für Reviews, Tickets oder Freigaben werden. Zweitens eignet sie sich als Mock-Backend für React-, Vue- oder Astro-Oberflächen. Drittens lässt sich das Muster auf Kontaktformulare oder Newsletter-Anmeldungen übertragen, wenn title durch email und message ersetzt wird.
Claude Code präzise beauftragen
Gib Claude Code Stack, Endpunkte, Fehlerverhalten und Prüfungen mit. Ein vager Auftrag wie “baue eine API” erzeugt oft lauffähigen, aber schwer prüfbaren Code.
Create a beginner Todo REST API with Express 5 and Node.js ES Modules.
Requirements:
- Provide package.json and server.js
- Add GET /health, GET /todos, GET /todos/:id, POST /todos, PUT /todos/:id, DELETE /todos/:id
- Accept JSON input
- title must be 1 to 120 characters; completed must be boolean
- Return JSON errors for 400, 404, and 500
- Keep PUT idempotent: repeating the same request should not change the final resource state
- Include curl requests and a node:test example
Der Idempotenz-Hinweis ist wichtig. PUT ersetzt eine Ressource. Wenn derselbe Body zweimal gesendet wird, soll der endgültige Zustand gleich bleiben. Wird updatedAt bei jedem identischen Request geändert, passen Code und REST-Erklärung nicht mehr zusammen.
Projekt erstellen
mkdir claude-rest-api
cd claude-rest-api
npm init -y
npm install express
Ersetze package.json:
{
"name": "claude-rest-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js",
"test": "node --test"
},
"dependencies": {
"express": "^5.0.0"
}
}
server.js
import express from "express";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const PORT = Number(process.env.PORT ?? 3000);
function createHttpError(status, message, details) {
const error = new Error(message);
error.status = status;
error.details = details;
return error;
}
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function parseId(rawId) {
const id = Number(rawId);
return Number.isInteger(id) && id > 0 ? id : null;
}
function validateTodoInput(body, { partial = false } = {}) {
const errors = [];
if (!partial || hasOwn(body, "title")) {
if (typeof body.title !== "string" || body.title.trim().length === 0) {
errors.push({ field: "title", message: "title is required" });
} else if (body.title.trim().length > 120) {
errors.push({ field: "title", message: "title must be 120 characters or fewer" });
}
}
if (!partial || hasOwn(body, "completed")) {
if (typeof body.completed !== "boolean") {
errors.push({ field: "completed", message: "completed must be a boolean" });
}
}
return errors;
}
export function createApp() {
const app = express();
let nextId = 3;
const todos = [
{
id: 1,
title: "Read MDN HTTP status docs",
completed: false,
updatedAt: "2026-06-03T00:00:00.000Z"
},
{
id: 2,
title: "Ask Claude Code to review API errors",
completed: true,
updatedAt: "2026-06-03T00:00:00.000Z"
}
];
app.use(express.json({ limit: "32kb" }));
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
app.get("/todos", (req, res) => {
res.json({ data: todos, count: todos.length });
});
app.get("/todos/:id", (req, res, next) => {
const id = parseId(req.params.id);
if (!id) return next(createHttpError(400, "id must be a positive integer"));
const todo = todos.find((item) => item.id === id);
if (!todo) return next(createHttpError(404, "todo not found"));
res.json({ data: todo });
});
app.post("/todos", (req, res, next) => {
const errors = validateTodoInput(req.body);
if (errors.length > 0) {
return next(createHttpError(400, "invalid request body", errors));
}
const todo = {
id: nextId,
title: req.body.title.trim(),
completed: req.body.completed,
updatedAt: new Date().toISOString()
};
nextId += 1;
todos.push(todo);
res.status(201).location(`/todos/${todo.id}`).json({ data: todo });
});
app.put("/todos/:id", (req, res, next) => {
const id = parseId(req.params.id);
if (!id) return next(createHttpError(400, "id must be a positive integer"));
const errors = validateTodoInput(req.body);
if (errors.length > 0) {
return next(createHttpError(400, "invalid request body", errors));
}
const index = todos.findIndex((item) => item.id === id);
if (index === -1) return next(createHttpError(404, "todo not found"));
const previous = todos[index];
const nextTodo = {
...previous,
title: req.body.title.trim(),
completed: req.body.completed
};
const changed =
previous.title !== nextTodo.title || previous.completed !== nextTodo.completed;
todos[index] = {
...nextTodo,
updatedAt: changed ? new Date().toISOString() : previous.updatedAt
};
res.json({ data: todos[index] });
});
app.delete("/todos/:id", (req, res, next) => {
const id = parseId(req.params.id);
if (!id) return next(createHttpError(400, "id must be a positive integer"));
const index = todos.findIndex((item) => item.id === id);
if (index === -1) return next(createHttpError(404, "todo not found"));
todos.splice(index, 1);
res.status(204).send();
});
app.use((req, res, next) => {
next(createHttpError(404, `route not found: ${req.method} ${req.originalUrl}`));
});
app.use((err, req, res, next) => {
const status = Number.isInteger(err.status) && err.status >= 400 ? err.status : 500;
const body = {
error: {
status,
message: status === 500 ? "Internal Server Error" : err.message
}
};
if (err.details) {
body.error.details = err.details;
}
res.status(status).json(body);
});
return app;
}
const currentFile = fileURLToPath(import.meta.url);
const startedFile = process.argv[1] ? resolve(process.argv[1]) : "";
if (startedFile === currentFile) {
createApp().listen(PORT, () => {
console.log(`REST API listening on http://localhost:${PORT}`);
});
}
Wichtig ist die klare Form: Erfolg kommt als { data: ... }, Fehler kommen als { error: ... }. Dadurch kann ein Frontend zuverlässig entscheiden, was angezeigt oder geloggt werden soll.
Mit curl testen
npm run dev
curl -i http://localhost:3000/health
curl -i http://localhost:3000/todos
curl -i -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title":"Write API tests","completed":false}'
curl -i -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title":"","completed":false}'
curl -i -X PUT http://localhost:3000/todos/1 \
-H "Content-Type: application/json" \
-d '{"title":"Read MDN HTTP status docs","completed":true}'
curl -i -X DELETE http://localhost:3000/todos/2
Prüfe nicht nur Erfolg. Eine API muss auch bei falscher Eingabe und fehlenden IDs verständlich antworten.
Kleiner node:test
Lege server.test.js an:
import assert from "node:assert/strict";
import test from "node:test";
import { createApp } from "./server.js";
function listen(app) {
return new Promise((resolve) => {
const server = app.listen(0, () => resolve(server));
});
}
async function request(baseUrl, path, options = {}) {
const response = await fetch(`${baseUrl}${path}`, {
headers: { "Content-Type": "application/json", ...options.headers },
...options
});
const text = await response.text();
return {
response,
body: text ? JSON.parse(text) : null
};
}
test("creates, updates, and deletes a todo", async (t) => {
const server = await listen(createApp());
t.after(() => server.close());
const baseUrl = `http://127.0.0.1:${server.address().port}`;
const created = await request(baseUrl, "/todos", {
method: "POST",
body: JSON.stringify({ title: "Test the API", completed: false })
});
assert.equal(created.response.status, 201);
assert.equal(created.body.data.title, "Test the API");
const id = created.body.data.id;
const updated = await request(baseUrl, `/todos/${id}`, {
method: "PUT",
body: JSON.stringify({ title: "Test the API", completed: true })
});
assert.equal(updated.response.status, 200);
assert.equal(updated.body.data.completed, true);
const deleted = await request(baseUrl, `/todos/${id}`, { method: "DELETE" });
assert.equal(deleted.response.status, 204);
});
test("rejects invalid todo input", async (t) => {
const server = await listen(createApp());
t.after(() => server.close());
const baseUrl = `http://127.0.0.1:${server.address().port}`;
const result = await request(baseUrl, "/todos", {
method: "POST",
body: JSON.stringify({ title: "", completed: false })
});
assert.equal(result.response.status, 400);
assert.equal(result.body.error.details[0].field, "title");
});
npm test
Diese Tests sind klein, aber nützlich. Wenn Claude Code später eine Datenbank einbauen soll, kann es die bestehende Testsuite als Grenze verwenden.
Typische Fehler
Vermeide Endpunkte wie /getTodos. Nutze Methoden: GET /todos, POST /todos, PUT /todos/:id.
Gib nicht immer 200 zurück. Schlechte Eingaben sind 400, fehlende Ressourcen 404, erfolgreiche Erstellung 201, erfolgreiche Löschung ohne Body 204.
Verlasse dich nicht nur auf Frontend-Validierung. Die API kann direkt aufgerufen werden und muss selbst prüfen.
Gib keine Stacktraces an Clients aus. Serverlogs dürfen Details enthalten, API-Antworten sollten kontrolliert bleiben.
Achte auf Idempotenz. Wenn ein PUT bei jedem identischen Aufruf neue Zufallswerte oder Zeitstempel schreibt, ist der Endzustand nicht mehr stabil.
Nächste Schritte und CTA
Baue danach nur eine Sache auf einmal ein: erst Datenbank, dann stärkere Schemavalidierung, dann API-Dokumentation, dann Authentifizierung. Für Fehlerdesign hilft auch Claude Code Error Handling Patterns.
Wenn du das mit deinem eigenen Projekt üben willst, nutze zuerst das kostenlose Cheat Sheet. Wiederverwendbare Prompts, Review-Checklisten und CLAUDE.md-Vorlagen findest du in der Claude Code Produktbibliothek. Für Team-Onboarding, Rechtekonzepte und API-Review-Prozesse passt Claude Code Training und Beratung.
Beim praktischen Testen dieses Artikels war der größte Lerneffekt nicht der CRUD-Code selbst, sondern der direkte Vergleich von Erfolg und Fehler: leerer title ergibt 400, unbekannte IDs ergeben 404, und wiederholte PUT-Requests behalten denselben Endzustand. Dadurch wird REST für Einsteiger zu einem Vertrag zwischen Frontend und Backend, nicht nur zu einer Sammlung von URLs.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.