NUCBA
20 de enero de 2026
programación

APIs REST: decisiones de diseño que hacen la diferencia

Diseñar APIs REST no es solo seguir convenciones HTTP. Te mostramos las decisiones que separan una API funcional de una que escala.

Equipo NUCBA

Equipo NUCBA

7 min de lectura

Cuando arrancás a desarrollar el backend de tu app, las primeras preguntas te llueven: ¿cómo estructuro los endpoints? ¿qué devuelvo cuando algo falla? ¿paginación, versionado, autenticación? Diseñar APIs REST bien no es solo seguir las convenciones HTTP, es tomar decisiones que después te van a ahorrar (o costar) semanas de refactor.

Este artículo no es un tutorial de Express o FastAPI. Es un recorrido por las decisiones concretas de diseño que separan una API funcional de una que escala, se mantiene y no te hace odiar tu propio código a los seis meses.

Por qué REST sigue siendo la base del backend moderno

REST (Representational State Transfer) no es la única forma de construir APIs, pero sigue siendo la más común. GraphQL tiene casos de uso específicos, gRPC brilla en microservicios internos, pero REST es el estándar de facto cuando estás construyendo un backend que va a consumir un frontend web, una app móvil o servicios de terceros.

¿Por qué? Porque REST aprovecha HTTP tal como es: métodos (GET, POST, PUT, DELETE), códigos de estado, headers. No inventás un protocolo nuevo, usás la web como fue diseñada.

Conceptos básicos que no podés saltear:

  • Recursos, no acciones: En REST, todo es un recurso. No /getUsers, sino /users. No /createOrder, sino POST /orders.
  • Métodos HTTP con semántica clara: GET lee, POST crea, PUT/PATCH actualiza, DELETE elimina.
  • Stateless: Cada request lleva toda la info necesaria (generalmente un token). El servidor no guarda sesión.
  • Representaciones: El mismo recurso puede devolverse en JSON, XML o lo que negocies con Accept.

Estos principios no son dogma, son convenciones que hacen tu API predecible. Y predecible es mantenible.

Estructura de endpoints: nombres, verbos y jerarquías

Acá es donde más inconsistencias se ven. Un equipo nombra /api/user/get, otro /users, otro /user-list. Todas funcionan, pero solo una escala.

Reglas que te salvan:

  1. Sustantivos en plural: /users, /orders, /products. Consistencia total.
  2. Jerarquía para relaciones: Si un recurso depende de otro, anidalo: /users/123/orders. Pero cuidado: más de dos niveles se vuelve complejo.
  3. Filtros en query params: No /users/active, sino /users?status=active. Los filtros no son recursos.
  4. Versionado explícito: /v1/users o header Accept: application/vnd.api.v1+json. Elegí uno y mantenelo.

Ejemplo de estructura coherente:

GET    /v1/users              # Lista usuarios
GET    /v1/users/123          # Usuario específico
POST   /v1/users              # Crear usuario
PATCH  /v1/users/123          # Actualizar parcial
DELETE /v1/users/123          # Eliminar
GET    /v1/users/123/orders   # Órdenes del usuario 123

Esto no es magia, es previsibilidad. Cualquiera que conozca REST puede inferir cómo funciona tu API sin leer docs.

Códigos de estado HTTP: hablá el idioma del protocolo

Los códigos de estado existen por algo. Devolver siempre 200 OK con { "error": true } en el body es una mala práctica que rompe cacheo, logging y cualquier cliente HTTP bien diseñado.

Los que más usás:

  • 200 OK: Request exitoso con respuesta.
  • 201 Created: Recurso creado, idealmente con Location header al nuevo recurso.
  • 204 No Content: Exitoso pero sin body (típico en DELETE).
  • 400 Bad Request: Error del cliente, datos inválidos.
  • 401 Unauthorized: Falta autenticación (token inválido/ausente).
  • 403 Forbidden: Autenticado pero sin permisos.
  • 404 Not Found: Recurso no existe.
  • 500 Internal Server Error: Algo se rompió en el servidor.

Ejemplo de respuesta de error consistente:

{
  "error": {
    "code": "INVALID_EMAIL",
    "message": "El formato del email es inválido",
    "field": "email"
  }
}

Siempre con el código HTTP correcto (en este caso, 400). Los clientes pueden manejar errores genéricamente basándose en el código de estado, y específicamente con el body.

Paginación, filtros y performance desde el diseño

Una API que devuelve 10.000 usuarios en un array es una bomba de tiempo. La paginación no es opcional, es parte del contrato.

Dos enfoques comunes:

1. Offset-based (clásica):

GET /users?page=2&limit=20

Respuesta:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 350,
    "pages": 18
  }
}

Funciona, pero tiene problemas con datasets que cambian rápido (duplicados o saltos si se inserta/elimina durante la navegación).

2. Cursor-based (más robusta):

GET /users?cursor=eyJpZCI6MTIzfQ&limit=20

El cursor es un token opaco que apunta a la posición exacta. Más complejo de implementar, pero más consistente.

Filtros y ordenamiento:

GET /users?status=active&role=admin&sort=-created_at

Definí una sintaxis y respetala. El - para orden descendente es una convención adoptada por varias APIs (JSON:API, por ejemplo).

Autenticación y autorización en APIs REST

Una API sin auth es un invite a problemas. Las opciones más comunes:

JWT (JSON Web Tokens):

El cliente manda un token en el header Authorization: Bearer <token>. El servidor lo valida (firma + expiración) sin tocar la base de datos.

Ventajas: Stateless, escala fácil.
Desventajas: No podés revocar un token antes de que expire (salvo que mantengas una blacklist, lo cual rompe parte de la magia stateless).

Session-based:

El servidor guarda la sesión (en memoria, Redis, etc.) y el cliente manda un cookie.

Ventajas: Revocación inmediata.
Desventajas: Requiere estado compartido entre servidores.

OAuth2:

Para cuando tu API va a ser consumida por terceros o necesitás delegación de permisos (login con Google, GitHub, etc.).

En la práctica: Para la mayoría de apps, JWT con refresh tokens cubre el 80% de los casos. El refresh token vive más tiempo y se usa para pedir nuevos access tokens cortos. Así tenés lo mejor de ambos mundos.

Documentación y contratos: tu API es tan buena como sus docs

Podés tener la API más elegante del mundo, pero si nadie entiende cómo usarla, no sirve.

OpenAPI (Swagger):

El estándar de facto. Escribís un spec (YAML o JSON) que describe endpoints, parámetros, respuestas, schemas. Herramientas como Swagger UI generan docs interactivas automáticamente.

Ejemplo mínimo:

openapi: 3.0.0
info:
  title: Users API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Lista usuarios
      parameters:
        - name: status
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Lista de usuarios
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'

Las docs vivas ahorran horas de Slack preguntando "che, ¿qué parámetros recibe este endpoint?".

Errores comunes al diseñar APIs REST

Mezclar verbos HTTP sin criterio:
No uses POST para todo. GET debe ser idempotente (sin efectos secundarios).

Endpoints que hacen demasiado:
POST /users/register-and-send-email-and-create-profile es un code smell. Mejor dividir responsabilidades.

No versionar:
Cambiar el contrato sin avisar rompe a todos tus clientes. Versioná desde el día uno.

Devolver datos sensibles:
Nunca devuelvas passwords hasheados, tokens internos o datos de otros usuarios por error. Usá DTOs (Data Transfer Objects) para controlar qué exponés.

Ignorar CORS:
Si tu frontend está en otro dominio, vas a necesitar configurar CORS correctamente. No pongas Access-Control-Allow-Origin: * en producción sin pensarlo.

Testing de APIs: contract testing y end-to-end

Una API sin tests es una API que te va a despertar a las 3 AM.

Niveles de testing:

  1. Unitarios: Lógica de negocio aislada (servicios, validaciones).
  2. Integración: Controllers + DB, simulando requests HTTP.
  3. Contract testing: Validar que la API respeta el contrato (OpenAPI spec). Herramientas: Dredd, Prism.
  4. E2E: Flujos completos desde el cliente (Playwright, Cypress con backend real).

Ejemplo con Supertest (Node.js):

const request = require('supertest');
const app = require('./app');

describe('GET /users', () => {
  it('devuelve lista de usuarios', async () => {
    const res = await request(app)
      .get('/users')
      .expect('Content-Type', /json/)
      .expect(200);
    
    expect(res.body.data).toBeInstanceOf(Array);
  });
});

El testing no es burocracia, es la red de seguridad que te deja refactorizar sin miedo.

Preguntas frecuentes

¿Cuándo usar PUT vs PATCH?
PUT reemplaza el recurso completo, PATCH actualiza campos específicos. En la práctica, PATCH es más común porque rara vez querés reemplazar todo.

¿Necesito GraphQL en vez de REST?
GraphQL resuelve over-fetching y under-fetching, pero suma complejidad. Si tu frontend pide siempre los mismos datos, REST bien diseñado es suficiente. GraphQL brilla cuando tenés clientes con necesidades muy distintas.

¿Cómo manejo rate limiting?
Middleware que cuenta requests por IP o token. Devolvé 429 Too Many Requests con header Retry-After. Redis es común para trackear contadores distribuidos.

¿Qué hago con endpoints que tardan mucho?
Para operaciones largas (exports, procesamiento pesado), devolvé 202 Accepted con un job ID, y exponé un endpoint para consultar el estado: GET /jobs/abc123.

¿Te gustó este artículo?

Descubre nuestros cursos y carreras para llevar tus habilidades al siguiente nivel.