Diseño práctico de API con Claude Code: OpenAPI, pruebas y cambios incompatibles
Diseña REST APIs fiables con Claude Code: OpenAPI, mock, pruebas, versionado, seguridad y errores comunes.
Diseñar una API no consiste en inventar URLs bonitas. Consiste en definir un contrato: qué puede enviar el cliente, qué devuelve el servidor y cómo se explica un fallo.
Cuando ese contrato es ambiguo, frontend, apps móviles, integraciones externas, pruebas y observabilidad terminan interpretando cosas distintas. Repararlo al final cuesta mucho más que escribir un contrato pequeño y claro desde el principio.
Claude Code ayuda bastante, pero no conviene usarlo solo como generador de código. Úsalo como revisor de diseño: que redacte un OpenAPI, revise endpoints, genere mock y pruebas, y detecte cambios incompatibles antes de que lleguen a producción.
Trabaja con referencias oficiales: OpenAPI Specification, RFC 9110 HTTP Semantics, JSON Schema docs y OWASP API Security Top 10. Para implementar después, lee también desarrollo de APIs de producción, automatización de pruebas de API y versionado de API.
Qué significa diseñar una API
Una API es una interfaz para otro programa. Una pantalla para humanos puede explicar su intención con botones y textos; una API se explica con rutas, métodos HTTP, códigos de estado, nombres de campos, schemas, ejemplos y errores.
Para empezar, decide cinco cosas.
| Decisión | Explicación simple | Ejemplo |
|---|---|---|
| Recurso | El sustantivo que expone la API | orders, customers, invoices |
| Operación | Qué se hace con el recurso | GET, POST, PATCH, DELETE |
| Schema | Forma y reglas del JSON | items debe tener al menos un elemento |
| Error | Cómo se reporta un fallo | 400, 401, 403, 404, 422 con detalles |
| Compatibilidad | Cómo no romper clientes | Añadir un campo obligatorio rompe |
REST puede sonar abstracto, pero la regla práctica es simple: usa URLs con sustantivos y deja la acción al método HTTP. POST /orders suele ser mejor que POST /orders/create, y GET /orders/ord_123 es más claro que GET /getOrder?id=ord_123.
Flujo de trabajo con Claude Code
No pidas diseño, implementación, pruebas y documentación en una sola instrucción gigante. Divide el trabajo en pasos que puedas revisar.
flowchart TD
A["Resumir reglas de negocio"] --> B["Redactar contrato OpenAPI"]
B --> C["Revisar HTTP, schema y seguridad"]
C --> D["Generar mock y pruebas API"]
D --> E["Detectar cambios incompatibles en CI"]
E --> F["Implementar, documentar y publicar"]
OpenAPI es un contrato legible por máquinas para APIs HTTP. JSON Schema describe la forma y las restricciones del JSON. Los códigos HTTP dan una semántica común de éxito y fallo. Claude Code puede conectar estas piezas, pero la fuente de verdad sigue siendo la especificación oficial y tus pruebas.
En una prueba pequeña, Masa comprobó que pedir primero solo una lista de endpoints daba una base razonable. El problema apareció al añadir autenticación, paginación, idempotency key y detalle de errores después. Un prompt inicial que pregunta por recuperación del cliente produce un diseño más sólido.
Ejemplo para copiar y ejecutar
Este ejemplo no usa paquetes externos. Permite probar OpenAPI, un servidor mock y una comprobación de cambios incompatibles.
mkdir api-design-lab
cd api-design-lab
mkdir docs examples
node --version
Crea docs/openapi.yaml. La página oficial de OpenAPI muestra la versión publicada más reciente; aquí usamos 3.1 por compatibilidad práctica con herramientas.
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/v1/orders:
post:
summary: Create an order
operationId: createOrder
security:
- bearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"422":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
CreateOrderRequest:
type: object
required: [customerId, items]
properties:
customerId:
type: string
minLength: 3
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
minLength: 3
quantity:
type: integer
minimum: 1
Order:
type: object
required: [id, status, customerId, total]
properties:
id:
type: string
status:
type: string
enum: [accepted, cancelled]
customerId:
type: string
total:
type: integer
Problem:
type: object
required: [type, title, status, detail]
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
errors:
type: array
items:
type: object
Crea examples/mock-server.mjs.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
function readJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
body += chunk;
if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
});
req.on("end", () => {
if (!body) return resolve({});
try {
resolve(JSON.parse(body));
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function send(res, status, body, headers = {}) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"x-content-type-options": "nosniff",
...headers,
});
res.end(JSON.stringify(body, null, 2));
}
function problem(status, title, detail, errors = []) {
return {
type: "https://example.com/problems/request",
title,
status,
detail,
errors,
};
}
function validateOrder(input) {
const errors = [];
if (typeof input.customerId !== "string" || input.customerId.length < 3) {
errors.push({
path: "customerId",
message: "customerId must be a string with 3+ characters",
});
}
if (!Array.isArray(input.items) || input.items.length === 0) {
errors.push({ path: "items", message: "items must contain at least one item" });
}
for (const [index, item] of (input.items ?? []).entries()) {
if (typeof item.sku !== "string" || item.sku.length < 3) {
errors.push({
path: `items.${index}.sku`,
message: "sku must be a string with 3+ characters",
});
}
if (!Number.isInteger(item.quantity) || item.quantity < 1) {
errors.push({
path: `items.${index}.quantity`,
message: "quantity must be a positive integer",
});
}
}
return errors;
}
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (req.method === "GET" && url.pathname === "/health") {
return send(res, 200, { ok: true });
}
const customerMatch = url.pathname.match(/^\/v1\/customers\/([a-z0-9-]+)$/);
if (req.method === "GET" && customerMatch) {
return send(res, 200, {
id: customerMatch[1],
name: "Aki Tanaka",
plan: "pro",
});
}
if (req.method === "POST" && url.pathname === "/v1/orders") {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return send(
res,
400,
problem(400, "Missing Idempotency-Key", "POST /v1/orders requires the header.")
);
}
try {
const body = await readJson(req);
const errors = validateOrder(body);
if (errors.length > 0) {
return send(res, 422, problem(422, "Invalid request body", "Fix errors.", errors));
}
return send(
res,
201,
{
id: `ord_${randomUUID()}`,
status: "accepted",
customerId: body.customerId,
total: 4200,
},
{ location: "/v1/orders/example" }
);
} catch {
return send(res, 400, problem(400, "Malformed JSON", "Request body must be JSON."));
}
}
return send(res, 404, problem(404, "Not found", `${req.method} ${url.pathname} is undefined.`));
});
server.listen(3000, () => {
console.log("Mock API running at http://localhost:3000");
});
Ejecuta el servidor y llama la API desde otra terminal.
node examples/mock-server.mjs
curl -i http://localhost:3000/health
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-001" \
-d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-002" \
-d '{"customerId":"x","items":[]}'
Crea examples/contract-check.mjs. Falla a propósito para mostrar cambios incompatibles.
import assert from "node:assert/strict";
const previous = {
paths: {
"/v1/orders": {
post: {
request: {
required: ["customerId", "items"],
properties: ["customerId", "items", "couponCode"],
},
response: {
required: ["id", "status", "customerId", "total"],
properties: ["id", "status", "customerId", "total"],
},
},
},
},
};
const next = structuredClone(previous);
next.paths["/v1/orders"].post.request.required.push("shippingAddress");
next.paths["/v1/orders"].post.response.properties =
next.paths["/v1/orders"].post.response.properties.filter((name) => name !== "total");
function diffContract(oldSpec, newSpec) {
const breaking = [];
for (const [path, methods] of Object.entries(oldSpec.paths)) {
for (const [method, oldOperation] of Object.entries(methods)) {
const newOperation = newSpec.paths[path]?.[method];
if (!newOperation) {
breaking.push(`${method.toUpperCase()} ${path} was removed`);
continue;
}
const oldRequired = new Set(oldOperation.request.required);
for (const field of newOperation.request.required) {
if (!oldRequired.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} now requires "${field}"`);
}
}
const newResponseFields = new Set(newOperation.response.properties);
for (const field of oldOperation.response.properties) {
if (!newResponseFields.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} removed response "${field}"`);
}
}
}
}
return breaking;
}
const breaking = diffContract(previous, next);
console.log(breaking.join("\n") || "No breaking changes found");
assert.equal(breaking.length, 0, "Breaking API changes detected");
node examples/contract-check.mjs
En este caso, fallar es correcto. El script detecta un nuevo campo obligatorio y un campo de respuesta eliminado.
Prompts para Claude Code
Separa redacción, revisión, generación de ejemplos y comprobación de compatibilidad.
claude -p "
Crea un borrador OpenAPI en docs/openapi.yaml para una API de pedidos e-commerce.
Recursos: customers, orders, invoices.
Incluye summary, operationId, requestBody, responses, examples y bearerAuth.
Usa OpenAPI 3.1 y restricciones de estilo JSON Schema.
"
claude -p "
Revisa docs/openapi.yaml como revisor de diseño de API.
Devuelve primero Findings por severidad y no edites archivos todavía.
Comprueba semántica de method/status según RFC 9110, schemas ambiguos,
pagination, idempotency, autenticación y riesgos comunes de OWASP API Security.
"
claude -p "
Genera un Mock server Node.js y ejemplos de API tests desde docs/openapi.yaml.
Cubre éxito, fallo de autenticación, fallo de validación y recurso inexistente.
Mantén líneas largas por debajo de 150 caracteres y añade comandos al README.
"
claude -p "
Compara el docs/openapi.yaml actual con la versión de HEAD.
Lista primero cambios incompatibles antes de sugerir ediciones.
Revisa paths eliminados, nuevos campos obligatorios, campos de respuesta eliminados,
cambios de status code y cambios de auth scope.
"
Casos de uso reales
El primer caso es una API de pedidos para SaaS. Panel de administración, facturación, emails y exportaciones contables leen el mismo Order. Si total, moneda, impuestos y cancelaciones no están claros, cada integración inventará su regla.
El segundo caso es una API de perfil para móvil. Las versiones antiguas de una app pueden seguir activas durante meses. Borrar un campo o cambiar el significado de un enum puede romper clientes que no se actualizan rápido.
El tercer caso es una API B2B para partners. Los desarrolladores externos no conocen tus convenciones internas. Necesitan códigos de error estables, rate limits, reglas de retry, sandbox y ejemplos.
El cuarto caso es una API interna de administración. Interna no significa segura por defecto. La autorización por objeto importa: un usuario no debería leer pedidos de otro tenant solo por conocer el ID.
Fallos y trampas
Un fallo común es meter verbos en la ruta: /cancelOrder, /getUserOrders, /updateOrderStatus. Con el tiempo aparecen nombres inconsistentes. Modela recursos primero y usa subrecursos cuando haga falta.
Otro fallo es devolver 200 para todos los errores de negocio. Eso complica monitoreo, SDKs, retries y manejo de errores. Usa 400 para input mal formado, 401 para falta de autenticación, 403 para prohibido, 404 para recurso inexistente y 422 para input semánticamente inválido.
Olvidar la seguridad de reintentos en POST también cuesta caro. Crear pedidos o iniciar pagos puede repetirse tras un timeout. Diseña Idempotency-Key desde el inicio.
La ambigüedad de schema crea bugs lentos. Decide si null significa borrar, desconocido o no enviado. Define campos requeridos, mínimos, límites de paginación, zonas horarias y comportamiento de propiedades extra.
Versionado, errores, schema y seguridad
Versionar no es solo añadir /v1. El equipo necesita reglas compartidas sobre qué rompe clientes. Añadir un campo opcional suele ser seguro; añadir un campo obligatorio, eliminar una respuesta, cambiar un status o endurecer permisos puede ser incompatible.
El diseño de errores debe decir qué hacer después. Invalid request no basta. Usa una forma estable con type, title, status, detail y errors por campo cuando sea útil. No expongas stack traces, SQL ni IDs internos en producción.
El schema debe expresar restricciones, no solo ejemplos. Formato de ID, longitud mínima, límites de array, paginación y fechas reducen la cantidad de suposiciones del cliente.
La seguridad separa autenticación de autorización. Un bearer token no sirve si GET /orders/{id} devuelve pedidos de otro tenant. Evita API keys en query string, reduce datos sensibles, aplica rate limit y guarda logs de auditoría.
CTA de monetización y consultoría
Quien busca diseño de API suele tener un proyecto concreto: publicar una integración, estabilizar un backend móvil o crear reglas de revisión para el equipo. El artículo debe demostrar que el flujo se puede aplicar, no solo explicar teoría.
ClaudeCodeLab puede ayudar con revisión de diseño API usando Claude Code, limpieza de OpenAPI, automatización de pruebas y checks de cambios incompatibles. Los equipos pueden empezar por formación y consultoría; los desarrolladores individuales pueden usar los recursos gratuitos.
Resultado verificado
Probé el código con Node v24.14.1. GET /health devolvió 200, un POST /v1/orders válido devolvió 201 y items vacío devolvió 422. contract-check.mjs falló intencionalmente y mostró el nuevo campo obligatorio y el campo de respuesta eliminado. El flujo completo va de prompts de Claude Code a OpenAPI, Mock, errores y una comprobación útil para CI.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.