Saltar al contenido

API-first: cómo diseñar servicios pensados para integrarse

A
abemon
| | 11 min de lectura | Escrito por profesionales
Compartir

La API es el producto

Hay un momento en la vida de toda empresa tecnológica en el que alguien dice “necesitamos una API”. Generalmente ocurre cuando un cliente pide integrarse, un partner necesita datos, o el equipo de frontend está harto de que el backend cambie sin aviso. Lo que debería haber ocurrido es que la API existiera desde el primer día.

API-first no es una moda. Es una decisión de arquitectura que dice: el contrato de la API se diseña antes que la implementación. No al revés. No como un subproducto del código. No como una capa que se pone encima al final. El contrato primero, la implementación después.

Por qué importa? Porque cuando la API es un afterthought, refleja las peculiaridades internas del sistema. Nombres de campos que solo tienen sentido si conoces el modelo de datos. Errores que devuelven stack traces en producción. Paginación inconsistente entre endpoints. Versiones que rompen sin aviso. Hemos visto todo esto en integraciones reales con ERPs y sistemas de terceros, y el coste en horas de debugging y frustración es enorme.

Contract-first con OpenAPI

OpenAPI (antes Swagger) es el estándar de facto para describir APIs REST. Un fichero OpenAPI es un contrato formal que define endpoints, parámetros, schemas de request/response, códigos de error, y autenticación. Escribirlo antes de implementar tiene beneficios concretos:

Paralelización del trabajo. Con el contrato definido, frontend y backend pueden trabajar en paralelo. Frontend genera mocks desde el spec. Backend implementa contra el contrato. QA escribe tests contra el contrato. Nadie espera a nadie.

Generación de código. Herramientas como openapi-generator producen SDKs de cliente en 40+ lenguajes, stubs de servidor, y tipos TypeScript directamente desde el spec. No es código perfecto, pero elimina la escritura manual de DTOs, validaciones y clientes HTTP.

Validación automática. Middleware como express-openapi-validator (Node.js) o connexion (Python) valida requests y responses contra el spec en runtime. Si un endpoint devuelve un campo que no está en el contrato, falla. Esto atrapa inconsistencias entre spec y código antes de que lleguen a producción.

Estructura de un buen spec OpenAPI

openapi: 3.1.0
info:
  title: Shipments API
  version: 2.0.0
  description: API for managing shipment lifecycle
servers:
  - url: https://api.example.com/v2
    description: Production
  - url: https://staging-api.example.com/v2
    description: Staging
paths:
  /shipments:
    get:
      operationId: listShipments
      summary: List shipments with pagination and filters
      parameters:
        - $ref: '#/components/parameters/PageCursor'
        - $ref: '#/components/parameters/PageSize'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ShipmentStatus'
      responses:
        '200':
          description: Paginated list of shipments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShipmentList'
        '401':
          $ref: '#/components/responses/Unauthorized'

Tres cosas que importan aquí:

  1. operationId en todos los endpoints. Es lo que usa el generador de código para nombrar métodos. Sin él, los nombres generados son ilegibles.
  2. Componentes reutilizables ($ref). Paginación, errores comunes, schemas compartidos. Definir una vez, usar en todas partes. Evita la inconsistencia que aparece cuando copias y pegas schemas.
  3. Servidores explícitos. El spec debe funcionar en Swagger UI sin modificaciones. Un desarrollador externo debería poder apuntar a staging y probar.

Versionado: la decisión que pagarás durante años

El versionado de APIs es una de esas decisiones que parecen menores al principio y se convierten en dolores de cabeza monumentales después. Hay tres estrategias principales:

Versionado en URL (/v1/shipments, /v2/shipments): La más explícita y la que recomendamos para APIs públicas. El consumidor sabe exactamente qué versión usa. El routing es trivial. La desventaja es que fuerza a mantener múltiples versiones desplegadas.

Versionado en header (Accept: application/vnd.api+json;version=2): Más limpia conceptualmente, pero más difícil de descubrir y de probar con herramientas sencillas. Funciona bien para APIs internas con consumidores controlados.

Sin versionado explícito (evolución compatible): Solo haces cambios compatibles hacia atrás. Nunca rompes. Funciona si tienes disciplina de hierro y pocos consumidores. Se vuelve impracticable cuando necesitas cambios estructurales.

Nuestra recomendación para la mayoría de los equipos: versionado en URL con una política de deprecación clara.

Política de deprecación

Una política de deprecación es un contrato social con tus consumidores. La nuestra es sencilla:

  • Mínimo 6 meses entre el anuncio de deprecación y la eliminación de una versión.
  • Anuncio vía header Sunset (RFC 8594) en todas las responses de la versión deprecada.
  • Anuncio vía email a los consumidores registrados.
  • Dashboard con métricas de uso por versión para saber cuándo es seguro eliminar.

Sin una política de deprecación, acabas manteniendo versiones fantasma que nadie usa (o que un consumidor crítico usa sin que lo sepas).

Errores: el diseño olvidado

La gestión de errores es donde más APIs fallan. No técnicamente (devuelven un 500 y se acabó), sino en la utilidad de la información que proporcionan al consumidor.

Estructura de errores estándar

Adoptar un formato de errores estándar como RFC 7807 (Problem Details for HTTP APIs) ahorra dolores de cabeza:

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The shipment weight exceeds the maximum for the selected carrier.",
  "instance": "/shipments/abc-123",
  "errors": [
    {
      "field": "weight",
      "message": "Must be less than 30000 grams for carrier DHL Express",
      "value": 35000
    }
  ]
}

Puntos clave:

  • type es una URI que identifica unívocamente el tipo de error. Permite al consumidor programar lógica específica por tipo de error sin parsear mensajes de texto.
  • detail es legible por humanos. Lo que el desarrollador verá en su consola cuando algo falle.
  • errors (extensión) proporciona detalle a nivel de campo para errores de validación. El frontend puede mostrar el error junto al campo correcto sin adivinar.

Códigos HTTP: usarlos correctamente

Suena básico, pero el 30-40% de las APIs que integramos usan los códigos HTTP mal:

CódigoSignificado realError común
400Request malformada (JSON inválido, campo faltante)Usar para lógica de negocio
401No autenticadoConfundir con 403
403Autenticado pero sin permisoConfundir con 401
404Recurso no existeDevolver para errores de lógica
409Conflicto (recurso ya existe, estado inválido)No usarlo
422Validación semántica fallida (los datos son JSON válido pero violan reglas de negocio)Usar 400 para todo
429Rate limit excedidoNo implementar rate limiting
500Error interno del servidorDevolver con stack traces

Si solo te llevas una cosa de esta sección: 400 es “no puedo parsear tu request” y 422 es “puedo parsearla pero no tiene sentido para el negocio”. La distinción importa porque el consumidor puede reintentar un 500 pero no un 422.

Paginación

Tres patrones, un ganador claro:

Offset-based (?page=3&per_page=20): Sencillo, intuitivo, y roto. Si se insertan o eliminan registros mientras el consumidor página, se saltan o duplican resultados. Para datos que cambian frecuentemente (pedidos, eventos), es inaceptable.

Cursor-based (?cursor=eyJpZCI6MTIzfQ&limit=20): Consistente independientemente de inserciones/eliminaciones. El cursor es un token opaco que apunta a la posición en el dataset. Cada página incluye el cursor para la siguiente. Es el patrón que usan Stripe, Shopify, y cualquier API seria.

Keyset-based (?after_id=123&limit=20): Similar a cursor pero con clave visible. Útil cuando el consumidor quiere empezar desde un punto conocido.

Nuestra recomendación: cursor-based para APIs públicas, con una response envelope estándar:

{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIzfQ",
    "has_more": true
  }
}

Autenticación y rate limiting

API keys para server-to-server. Simple, funcional. La key va en el header Authorization: Bearer sk_live_.... Nunca en la URL (se loguea en proxies y CDNs). Prefijar las keys con el entorno (sk_live_, sk_test_) evita el error clásico de usar producción en desarrollo.

OAuth 2.0 para accesos delegados. Cuando un usuario autoriza a una app a acceder en su nombre. Más complejo, pero necesario para integraciones donde el usuario final importa.

Rate limiting explicitado en headers. Siempre devolver X-RateLimit-Limit, X-RateLimit-Remaining, y X-RateLimit-Reset. El consumidor puede adaptar su comportamiento sin trial-and-error. Cuando se excede el límite, devolver 429 con un header Retry-After.

Documentación como código

La documentación de API que vive fuera del código se desactualiza. Siempre. Sin excepción. La única documentación fiable es la que se genera desde el spec OpenAPI o se valida contra él.

Herramientas que funcionan

  • Redocly: Genera documentación estática desde OpenAPI. Limpia, personalizable, y se integra en CI/CD.
  • Scalar: Alternativa moderna a Swagger UI. API reference interactiva con ejemplos ejecutables.
  • Mintlify/Fern: Documentación completa (guides + API reference) generada desde OpenAPI + markdown. Ideal para developer portals públicos.

Lo mínimo viable

Cada endpoint necesita:

  1. Descripción de qué hace (no cómo se implementa).
  2. Ejemplo de request completo con datos realistas (no "string" o "example").
  3. Ejemplo de response para el caso exitoso y para cada tipo de error.
  4. Código de ejemplo en al menos un lenguaje (curl + el lenguaje más común entre tus consumidores).

Lo que diferencia una buena documentación de una mediocre no es la cantidad. Es la calidad de los ejemplos. Un ejemplo con datos realistas ("tracking_number": "1Z999AA10123456784") vale más que diez páginas de descripción abstracta.

Testing de contratos

El contrato OpenAPI no sirve de nada si no se valida. Hay dos niveles:

Validación en CI: Herramientas como Spectral lintean el spec contra reglas de estilo. Es el equivalente a ESLint para APIs: nombres en camelCase, descriptions obligatorias, schemas reutilizados. Configurar Spectral con un ruleset corporativo estandariza el diseño entre equipos.

Contract testing: Herramientas como Prism (de Stoplight) o Schemathesis generan requests aleatorias a partir del spec y verifican que el servidor responde conforme al contrato. Pact es la alternativa cuando necesitas contract testing entre servicios (consumer-driven contracts).

Integrar ambos en el pipeline de CI significa que ningún cambio que rompa el contrato llega a producción. Eso es lo que separa una API profesionalmente diseñada de una que “funciona en mi máquina”.

API-first en la práctica: flujo de trabajo

  1. Diseño del spec en un fichero OpenAPI. Review en pull request como si fuera código. Los consumidores participan en la review.
  2. Generación de mocks con Prism o Mockoon. Frontend y consumidores externos empiezan a integrar contra mocks.
  3. Implementación del servidor contra el spec. Validación automática en runtime.
  4. Contract testing en CI. Spectral para linting, Schemathesis para fuzzing.
  5. Documentación generada automáticamente. Publicada en cada merge a main.
  6. Monitoring de uso real. Métricas por endpoint (latencia, error rate, volumen) para detectar qué se usa, qué se rompe, y qué deprecar.

El flujo parece más trabajo al principio. Lo es. Pero cada hora invertida en diseñar bien el contrato ahorra diez horas de debugging, reuniones de alineación, y soporte a integradores. En un ecosistema con 5+ consumidores, la diferencia es brutal.

Reflexión final

API-first es una apuesta por el largo plazo. Cuesta más al principio. Requiere disciplina. Obliga a pensar en los consumidores antes de escribir la primera línea de código. Pero las organizaciones que lo practican construyen sistemas que escalan no solo en tráfico, sino en el número de equipos y partners que pueden integrar sin que todo se desmonte. Y en un mundo donde la integración es la norma, esa capacidad vale más que cualquier feature individual.

Sobre el autor

A

abemon engineering

Equipo de ingenieria

Equipo multidisciplinar de ingenieria, datos e IA con sede en Canarias. Construimos, desplegamos y operamos soluciones de software a medida para empresas de cualquier escala.

Síguenos: LinkedIn GitHub