REST-APIs ultraschnell entwerfen, implementieren & testen mit Claude Code | Von der OpenAPI-Spezifikation bis zur Produktion
So entwickeln Sie REST-APIs von der OpenAPI-Spezifikation bis zur produktionsreifen TypeScript-Implementierung mit Claude Code. Mit Hono/Express/Fastify, zod-Validierung, vitest-Testgenerierung und vollständigen Codebeispielen.
Warum Claude Code ideal für das REST-API-Design ist
REST-API-Entwicklung besteht zu einem großen Teil aus sich wiederholenden Mustern: Endpunkt-Definitionen, Validierung, Fehlerbehandlung und Tests — alles von Hand geschrieben. Dabei verliert man leicht den Fokus auf die eigentliche Geschäftslogik.
Ich bin Masa, DX-Ingenieur und verantwortlich für die Modernisierung interner System-APIs. Vor Claude Code betrug mein Standardaufwand pro Endpunkt: „30 Min. Design → 2 Std. Implementierung → 1 Std. Tests”. Nach der vollständigen Integration von Claude Code sank das auf 10 Min. Design → 30 Min. Implementierung → 15 Min. Tests.
3 Gründe, warum Claude Code bei der API-Entwicklung überzeugt
- Versteht OpenAPI-Spezifikationen als Kontext — YAML einfügen und es generiert spezifikationskonforme Code sofort
- Beherrscht das TypeScript-Typsystem in der Tiefe — produziert typensicheren Code von Anfang an
- Kennt ein breites Spektrum an Testmustern — generiert Happy-Path-, Fehler- und Edge-Case-Tests in einem Durchlauf
1. OpenAPI-Spezifikation mit Claude Code automatisch generieren
Prompt-Vorlage
Beispiel-Prompt für eine „Task-Management-API”:
Erstelle eine OpenAPI-3.1-Spezifikation (YAML) für folgende Anforderungen.
Ressource: Task (Aufgabenverwaltung)
Felder:
- id: UUID
- title: string (1–200 Zeichen)
- description: string (optional)
- status: "todo" | "in_progress" | "done"
- priority: "low" | "medium" | "high" (Standard: medium)
- assignee_id: UUID (optional)
- due_date: ISO8601-Format (optional)
- created_at: ISO8601-Format
- updated_at: ISO8601-Format
Endpunkte:
- GET /tasks (Liste mit Paginierung)
- GET /tasks/:id
- POST /tasks
- PUT /tasks/:id (vollständige Aktualisierung)
- PATCH /tasks/:id (partielle Aktualisierung)
- DELETE /tasks/:id
Antwortformat: { data, meta, errors }
Fehlercodes: 400, 401, 403, 404, 422, 500
Authentifizierung: Bearer Token (Authorization-Header)
Generiertes OpenAPI-YAML (Auszug)
openapi: 3.1.0
info:
title: Task Management API
version: 1.0.0
description: OpenAPI-Spezifikation für die Task-Management-API
servers:
- url: https://api.example.com/v1
description: Produktion
- url: http://localhost:3000/v1
description: Entwicklung
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Task:
type: object
required: [id, title, status, priority, created_at, updated_at]
properties:
id:
type: string
format: uuid
title:
type: string
minLength: 1
maxLength: 200
status:
type: string
enum: [todo, in_progress, done]
priority:
type: string
enum: [low, medium, high]
default: medium
paths:
/tasks:
get:
summary: Aufgaben auflisten
security:
- bearerAuth: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Erfolg
2. Typsichere Handler-Implementierung mit Hono
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { taskRouter } from "./routes/tasks";
const app = new Hono();
// Middleware
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());
// Router registrieren
app.route("/v1/tasks", taskRouter);
// Globaler Fehler-Handler
app.onError((err, c) => {
console.error(err);
return c.json(
{
data: null,
meta: null,
errors: [{ code: "INTERNAL_ERROR", message: "Ein interner Serverfehler ist aufgetreten" }],
},
500
);
});
export default app;
Task-Router-Implementierung
// src/routes/tasks.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { TaskCreateSchema, TaskUpdateSchema, TaskPatchSchema, TaskQuerySchema } from "../schemas/task";
import { TaskService } from "../services/taskService";
export const taskRouter = new Hono();
const taskService = new TaskService();
// Aufgaben auflisten
taskRouter.get("/", zValidator("query", TaskQuerySchema), async (c) => {
const query = c.req.valid("query");
const result = await taskService.findAll(query);
return c.json({
data: result.tasks,
meta: { total: result.total, page: query.page, per_page: query.per_page },
errors: null,
});
});
// Aufgabe nach ID abrufen
taskRouter.get("/:id", async (c) => {
const id = c.req.param("id");
const task = await taskService.findById(id);
if (!task) {
return c.json(
{ data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Aufgabe nicht gefunden", field: "id" }] },
404
);
}
return c.json({ data: task, meta: null, errors: null });
});
// Aufgabe erstellen
taskRouter.post("/", zValidator("json", TaskCreateSchema), async (c) => {
const body = c.req.valid("json");
const task = await taskService.create(body);
return c.json({ data: task, meta: null, errors: null }, 201);
});
// Aufgabe löschen
taskRouter.delete("/:id", async (c) => {
const id = c.req.param("id");
const deleted = await taskService.delete(id);
if (!deleted) {
return c.json(
{ data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Aufgabe nicht gefunden", field: "id" }] },
404
);
}
return c.json({ data: null, meta: null, errors: null }, 204);
});
3. Zod-Validierungsschemata automatisch generieren
// src/schemas/task.ts
import { z } from "zod";
const TaskStatus = z.enum(["todo", "in_progress", "done"]);
const TaskPriority = z.enum(["low", "medium", "high"]);
export const TaskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1, "Titel muss mindestens 1 Zeichen lang sein").max(200, "Titel darf maximal 200 Zeichen lang sein"),
description: z.string().nullable(),
status: TaskStatus,
priority: TaskPriority,
assignee_id: z.string().uuid().nullable(),
due_date: z.string().datetime().nullable(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
export const TaskCreateSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
status: TaskStatus.default("todo"),
priority: TaskPriority.default("medium"),
assignee_id: z.string().uuid().optional(),
due_date: z.string().datetime().optional(),
});
export const TaskUpdateSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().nullable(),
status: TaskStatus,
priority: TaskPriority,
assignee_id: z.string().uuid().nullable(),
due_date: z.string().datetime().nullable(),
});
export const TaskPatchSchema = TaskUpdateSchema.partial();
export const TaskQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
per_page: z.coerce.number().int().min(1).max(100).default(20),
status: TaskStatus.optional(),
priority: TaskPriority.optional(),
});
export type Task = z.infer<typeof TaskSchema>;
export type TaskCreate = z.infer<typeof TaskCreateSchema>;
export type TaskQuery = z.infer<typeof TaskQuerySchema>;
Vorher/Nachher-Vergleich
| Kennzahl | Vor Claude Code | Nach Claude Code |
|---|---|---|
| Schema-Definitionszeit | 30–45 Min. pro Endpunkt | 5–10 Min. (inkl. Prompt) |
| Typfehlerrate | Häufig in späten Projektphasen | Nahezu null von Anfang an |
| Übersehene Validierungsfälle | Bei jedem Code-Review beanstandet | Edge Cases automatisch abgedeckt |
| Spezifikations-/Code-Abweichung | Veraltet nach 2–3 Wochen | Immer synchron (aus Spezifikation generiert) |
4. Tests automatisch mit Vitest generieren
// src/routes/tasks.test.ts
import { describe, it, expect, vi } from "vitest";
import app from "../index";
import { TaskService } from "../services/taskService";
vi.mock("../services/taskService");
const mockTask = {
id: "550e8400-e29b-41d4-a716-446655440000",
title: "Test-Aufgabe",
description: "Eine Aufgabe zum Testen",
status: "todo" as const,
priority: "medium" as const,
assignee_id: null,
due_date: null,
created_at: "2026-04-25T00:00:00.000Z",
updated_at: "2026-04-25T00:00:00.000Z",
};
describe("GET /v1/tasks", () => {
it("Happy Path: gibt Aufgabenliste zurück", async () => {
vi.mocked(TaskService.prototype.findAll).mockResolvedValue({ tasks: [mockTask], total: 1 });
const res = await app.request("/v1/tasks", { headers: { Authorization: "Bearer valid-token" } });
expect(res.status).toBe(200);
const body = await res.json();
expect(body.data).toHaveLength(1);
});
it("Fehlerpfad: per_page überschreitet Maximum", async () => {
const res = await app.request("/v1/tasks?per_page=101", { headers: { Authorization: "Bearer valid-token" } });
expect(res.status).toBe(400);
});
});
describe("POST /v1/tasks", () => {
it("Happy Path: erstellt eine Aufgabe", async () => {
vi.mocked(TaskService.prototype.create).mockResolvedValue(mockTask);
const res = await app.request("/v1/tasks", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
body: JSON.stringify({ title: "Neue Aufgabe" }),
});
expect(res.status).toBe(201);
});
it("Fehlerpfad: leerer Titel", async () => {
const res = await app.request("/v1/tasks", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
body: JSON.stringify({ title: "" }),
});
expect(res.status).toBe(400);
});
});
5. Fehlerbehandlungsmuster
// src/lib/errors.ts
export class AppError extends Error {
constructor(
public readonly code: string,
public readonly message: string,
public readonly statusCode: number,
public readonly field?: string
) {
super(message);
this.name = "AppError";
}
}
export const Errors = {
notFound: (resource: string) => new AppError("NOT_FOUND", `${resource} nicht gefunden`, 404),
unauthorized: () => new AppError("UNAUTHORIZED", "Authentifizierung erforderlich", 401),
forbidden: () => new AppError("FORBIDDEN", "Zugriff verweigert", 403),
conflict: (message: string) => new AppError("CONFLICT", message, 409),
internal: () => new AppError("INTERNAL_ERROR", "Ein interner Serverfehler ist aufgetreten", 500),
};
export function createErrorResponse(err: unknown) {
if (err instanceof AppError) {
return {
data: null, meta: null,
errors: [{ code: err.code, message: err.message, ...(err.field ? { field: err.field } : {}) }],
};
}
return { data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "Ein unerwarteter Fehler ist aufgetreten" }] };
}
6. Swagger-UI-Dokumentation automatisch generieren
// src/swagger.ts
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
const openApiSpec = YAML.parse(fs.readFileSync(path.resolve("./openapi.yaml"), "utf8"));
export function setupSwagger(app: OpenAPIHono) {
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
app.get("/openapi.json", (c) => c.json(openApiSpec));
console.log("Swagger UI: http://localhost:3000/docs");
}
3 Häufige Fallstricke
Fallstrick 1: Fehlende UUID-Validierung
Claude Code behandelt den :id-Pfadparameter manchmal als einfachen String. Ohne UUID-Format-Validierung können fehlerhafte Abfragen die Datenbank erreichen.
// SCHLECHT: Rohstring direkt an DB
const task = await db.findById(c.req.param("id"));
// GUT: UUID-Format zuerst validieren
const idSchema = z.string().uuid("Keine gültige UUID");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "Ungültiges ID-Format" }] }, 400);
}
const task = await db.findById(parsed.data);
Fallstrick 2: Fehlende Content-Type-Validierung
Bei PUT/POST ohne Content-Type: application/json behandelt Hono den Body als leeres Objekt. Die Zod-Validierung besteht und leere Daten werden gespeichert.
// src/middleware/contentType.ts
import { createMiddleware } from "hono/factory";
export const requireJson = createMiddleware(async (c, next) => {
if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
const contentType = c.req.header("Content-Type");
if (!contentType?.includes("application/json")) {
return c.json(
{ data: null, meta: null, errors: [{ code: "UNSUPPORTED_MEDIA_TYPE", message: "Content-Type: application/json ist erforderlich" }] },
415
);
}
}
await next();
});
Fallstrick 3: Zeitzonenproblem bei Datumsfeldern
z.string().datetime() akzeptiert nur UTC-Format. Wenn das Frontend 2026-04-25T09:00:00+01:00 sendet, wird es abgelehnt.
// SCHLECHT
due_date: z.string().datetime()
// GUT: Zeitzonenversatz erlauben
due_date: z.string().datetime({ offset: true }).optional()
Fazit
Durch die Integration von Claude Code in die REST-API-Entwicklung lässt sich die Zeit bis zur Produktionsreife erheblich reduzieren:
- OpenAPI-Spezifikation zuerst generieren — bildet die Grundlage für alle nachgelagerten Schritte
- Zod-Schemata aus der Spezifikation generieren — manuelle Erstellung führt zu Abweichungen
- Happy-Path-, Fehler- und Edge-Case-Tests explizit anfordern — ohne Anweisung werden nur Happy Paths geschrieben
- Fallstricke im Prompt vorwegnehmen — UUID-Validierung, Content-Type-Prüfung, Zeitzonenbehandlung
Seit der Einführung dieses Workflows sank die Entwicklungszeit pro Endpunkt um etwa 70 %.
Claude Code-Ausgaben sollten dennoch immer reviewed werden — insbesondere für Sicherheit und Geschäftslogik. Claude Code ist ein Werkzeug zur schnellen Ersterstellung; die endgültige Qualitätsverantwortung liegt beim Entwickler.
Verwandte Artikel:
Bring deinen Claude-Code-Workflow aufs nächste Level
50 in der Praxis erprobte Prompt-Vorlagen zum direkten Copy-and-paste in Claude Code.
Kostenloses PDF: Claude-Code-Spickzettel in 5 Minuten
Trag einfach deine E-Mail-Adresse ein – wir senden dir den A4-Spickzettel als PDF sofort zu.
Wir behandeln deine Daten sorgfältig und senden niemals Spam.
Über den Autor
Masa
Ingenieur, der Claude Code intensiv nutzt. Betreibt claudecode-lab.com, ein Tech-Medium in 10 Sprachen mit über 2.000 Seiten.
Ähnliche Artikel
Claude Code Komplettanleitung 2026 | In 7 Schritten vom Einstieg zur produktiven Nutzung
Die vollständige Einführung in Claude Code für Anfänger. Von der Installation bis zur Integration in den echten Entwickler-Workflow — mit allen Stolperfallen, auf die Masa anfangs gestoßen ist.
REST API mit Claude Code erstellen | Praxisleitfaden für Einsteiger
REST API Grundlagen mit Claude Code lernen. Ein praktischer Leitfaden für Endpunkt-Design, Validierung und Fehlerbehandlung — mit kopierfertigem Code.
Claude Code vs Gemini CLI 2026 Vergleich | Wie unterscheidet sich Googles KI wirklich?
Praxisvergleich von Claude Code und Gemini CLI durch DX-Ingenieur Masa. Preise, Autonomie, Kontextfenster und Ökosystem im Test. Mit Entscheidungsflussdiagramm für die richtige Toolwahl.