Tips & Tricks

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

  1. Versteht OpenAPI-Spezifikationen als Kontext — YAML einfügen und es generiert spezifikationskonforme Code sofort
  2. Beherrscht das TypeScript-Typsystem in der Tiefe — produziert typensicheren Code von Anfang an
  3. 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

KennzahlVor Claude CodeNach Claude Code
Schema-Definitionszeit30–45 Min. pro Endpunkt5–10 Min. (inkl. Prompt)
TypfehlerrateHäufig in späten ProjektphasenNahezu null von Anfang an
Übersehene ValidierungsfälleBei jedem Code-Review beanstandetEdge Cases automatisch abgedeckt
Spezifikations-/Code-AbweichungVeraltet nach 2–3 WochenImmer 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:

  1. OpenAPI-Spezifikation zuerst generieren — bildet die Grundlage für alle nachgelagerten Schritte
  2. Zod-Schemata aus der Spezifikation generieren — manuelle Erstellung führt zu Abweichungen
  3. Happy-Path-, Fehler- und Edge-Case-Tests explizit anfordern — ohne Anweisung werden nur Happy Paths geschrieben
  4. 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:

#claude-code #rest-api #openapi #typescript #backend

Bring deinen Claude-Code-Workflow aufs nächste Level

50 in der Praxis erprobte Prompt-Vorlagen zum direkten Copy-and-paste in Claude Code.

Kostenlos

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.

Masa

Ü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.