Tips & Tricks

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

  1. Entiende las especificaciones OpenAPI como contexto — pega el YAML y genera código conforme a la especificación de inmediato
  2. Domina el sistema de tipos de TypeScript — produce código con tipado seguro desde el principio
  3. 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étricaAntes de Claude CodeDespués de Claude Code
Tiempo de definición de esquemas30–45 min por endpoint5–10 min (incluido el prompt)
Tasa de errores de tiposFrecuente en fases tardíasCasi cero desde el inicio
Validaciones omitidasDetectadas en cada revisión de códigoCasos límite cubiertos automáticamente
Divergencia especificación/códigoDesactualizada en 2–3 semanasSiempre 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:

  1. Generar la especificación OpenAPI primero — es la base de todos los pasos posteriores
  2. Generar los esquemas Zod desde la especificación — escribirlos a mano provoca divergencias
  3. Solicitar explícitamente tests de camino feliz, error y casos límite — sin instrucción, solo se escriben caminos felices
  4. 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:

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

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.

Gratis

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.

Masa

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.