Diseña, implementa y prueba APIs REST a velocidad máxima con Claude Code | De OpenAPI a producción
Aprende a desarrollar APIs REST de principio a fin con Claude Code: desde la generación de especificaciones OpenAPI hasta el código TypeScript listo para producción. Incluye Hono, validación con zod, generación de tests con vitest y ejemplos funcionales.
Por qué Claude Code es ideal para el diseño de APIs REST
El desarrollo de APIs REST está lleno de patrones repetitivos: definiciones de endpoints, validación, manejo de errores y pruebas — todo escrito a mano. Es difícil mantener el foco en la lógica de negocio mientras se lidia con ese boilerplate.
Soy Masa, ingeniero de DX a cargo de la modernización de APIs en sistemas internos. Antes de Claude Code, mi coste estándar por endpoint era: “30 min de diseño → 2 h de implementación → 1 h de pruebas”. Tras integrar Claude Code completamente: 10 min de diseño → 30 min de implementación → 15 min de pruebas.
3 razones por las que Claude Code destaca en el desarrollo de APIs
- Entiende las especificaciones OpenAPI como contexto — pega el YAML y genera código conforme a la especificación de inmediato
- Domina el sistema de tipos de TypeScript — produce código con tipado seguro desde el principio
- Conoce un amplio abanico de patrones de prueba — genera pruebas del camino feliz, de error y de casos límite en una sola ejecución
1. Generar la especificación OpenAPI con Claude Code
Ejemplo de prompt
Genera una especificación OpenAPI 3.1 (YAML) para los siguientes requisitos.
Recurso: Task (gestión de tareas)
Campos:
- id: UUID
- title: string (1–200 caracteres)
- description: string (opcional)
- status: "todo" | "in_progress" | "done"
- priority: "low" | "medium" | "high" (por defecto: medium)
- assignee_id: UUID (opcional)
- due_date: formato ISO8601 (opcional)
- created_at: formato ISO8601
- updated_at: formato ISO8601
Endpoints:
- GET /tasks (lista con paginación)
- GET /tasks/:id
- POST /tasks
- PUT /tasks/:id (actualización completa)
- PATCH /tasks/:id (actualización parcial)
- DELETE /tasks/:id
Formato de respuesta: { data, meta, errors }
Códigos de error: 400, 401, 403, 404, 422, 500
Autenticación: Bearer Token (cabecera Authorization)
YAML OpenAPI generado (extracto)
openapi: 3.1.0
info:
title: Task Management API
version: 1.0.0
description: Especificación OpenAPI para la API de gestión de tareas
servers:
- url: https://api.example.com/v1
description: Producción
- url: http://localhost:3000/v1
description: Desarrollo
components:
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
2. Implementación de handlers con tipado seguro en 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());
// Registro de rutas
app.route("/v1/tasks", taskRouter);
// Manejador global de errores
app.onError((err, c) => {
console.error(err);
return c.json(
{ data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "Se produjo un error interno del servidor" }] },
500
);
});
export default app;
Implementación del router de tareas
// 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();
// Listar tareas
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,
});
});
// Obtener tarea por ID
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: "Tarea no encontrada", field: "id" }] },
404
);
}
return c.json({ data: task, meta: null, errors: null });
});
// Crear tarea
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);
});
// Eliminar tarea
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: "Tarea no encontrada", field: "id" }] },
404
);
}
return c.json({ data: null, meta: null, errors: null }, 204);
});
3. Generación automática de esquemas de validación con Zod
// 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, "El título debe tener al menos 1 carácter").max(200, "El título no puede superar 200 caracteres"),
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>;
Comparativa antes/después
| Métrica | Antes de Claude Code | Después de Claude Code |
|---|---|---|
| Tiempo de definición de esquemas | 30–45 min por endpoint | 5–10 min (incluido el prompt) |
| Tasa de errores de tipos | Frecuente en fases tardías | Casi cero desde el inicio |
| Validaciones omitidas | Detectadas en cada revisión de código | Casos límite cubiertos automáticamente |
| Divergencia especificación/código | Desactualizada en 2–3 semanas | Siempre sincronizada (generada desde la especificación) |
4. Generación automática de tests con Vitest
// 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: "Tarea de prueba",
description: "Una tarea para testear",
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("camino feliz: devuelve la lista de tareas", 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("error: per_page supera el máximo", 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("camino feliz: crea una tarea", 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: "Nueva tarea" }),
});
expect(res.status).toBe(201);
});
it("error: título vacío", 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. Patrones de manejo de errores
// 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} no encontrado`, 404),
unauthorized: () => new AppError("UNAUTHORIZED", "Se requiere autenticación", 401),
forbidden: () => new AppError("FORBIDDEN", "No tienes permiso para acceder a este recurso", 403),
conflict: (message: string) => new AppError("CONFLICT", message, 409),
internal: () => new AppError("INTERNAL_ERROR", "Se produjo un error interno del servidor", 500),
};
6. Generar documentación con Swagger UI
// 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 errores comunes que hay que evitar
Error 1: Validación de UUID omitida
Claude Code a veces trata el parámetro de ruta :id como una cadena simple. Sin validación de formato UUID, pueden llegar consultas malformadas a la base de datos.
// MAL: cadena cruda enviada directamente a la BD
const task = await db.findById(c.req.param("id"));
// BIEN: validar formato UUID primero
const idSchema = z.string().uuid("No es un UUID válido");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "Formato de ID inválido" }] }, 400);
}
const task = await db.findById(parsed.data);
Error 2: Validación del Content-Type omitida
Para PUT/POST sin Content-Type: application/json, Hono trata el cuerpo como objeto vacío. La validación de zod pasa y se guardan datos vacíos.
// 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: "Se requiere Content-Type: application/json" }] },
415
);
}
}
await next();
});
Error 3: Problema de zona horaria en campos de fecha
z.string().datetime() solo acepta formato UTC. Si el frontend envía 2026-04-25T09:00:00-05:00, es rechazado.
// MAL
due_date: z.string().datetime()
// BIEN: permitir desplazamiento de zona horaria
due_date: z.string().datetime({ offset: true }).optional()
Conclusión
Integrar Claude Code en el desarrollo de APIs REST reduce drásticamente el tiempo hasta la puesta en producción:
- Generar la especificación OpenAPI primero — es la base de todos los pasos posteriores
- Generar los esquemas Zod desde la especificación — escribirlos a mano provoca divergencias
- Solicitar explícitamente tests de camino feliz, error y casos límite — sin instrucción, solo se escriben caminos felices
- Incluir los errores comunes en el prompt desde el principio — validación UUID, Content-Type, zonas horarias
Desde que adoptamos este flujo de trabajo, el tiempo de desarrollo por endpoint se redujo en un ~70 %.
Dicho esto, nunca envíes a producción el código de Claude Code sin revisarlo. La seguridad y la lógica de negocio siempre necesitan ojos humanos. Claude Code es una máquina de borradores rápidos; la responsabilidad final de la calidad recae en el desarrollador.
Artículos relacionados:
Lleva tu flujo con Claude Code al siguiente nivel
50 plantillas de prompts probadas en producción, listas para copiar y pegar en Claude Code.
PDF gratuito: Hoja de trucos de Claude Code en 5 minutos
Solo deja tu correo y te enviaremos al instante la hoja de trucos en una página A4.
Cuidamos tus datos personales y nunca enviamos spam.
Sobre el autor
Masa
Ingeniero apasionado por Claude Code. Dirige claudecode-lab.com, un medio tecnológico en 10 idiomas con más de 2.000 páginas.
Artículos relacionados
Guía completa para empezar con Claude Code 2026 | De cero a usarlo en tu trabajo real en 7 pasos
La guía definitiva para quienes usan Claude Code por primera vez. Desde la instalación hasta integrarlo en tu flujo de desarrollo real — con todos los tropiezos que Masa tuvo al comenzar.
Crear una REST API con Claude Code | Guía práctica para principiantes
Aprendé los fundamentos de REST API con Claude Code. Una guía práctica que cubre diseño de endpoints, validación y manejo de errores — con código listo para copiar.
Claude Code vs Gemini CLI 2026 Comparativa Profunda | ¿En qué se diferencia la IA de Google?
Comparativa práctica de Claude Code y Gemini CLI por el ingeniero DX Masa. Precios, autonomía, ventana de contexto y ecosistema analizados. Incluye diagrama de decisión para elegir la herramienta correcta.