Construir una API REST bien diseñada es una de las habilidades más importantes para los desarrolladores modernos. Una gran API es intuitiva, consistente, segura y fácil de mantener. Esta guía completa cubre todo, desde convenciones de nomenclatura de URL y métodos HTTP hasta autenticación, paginación, limitación de velocidad y errores comunes.
Convenciones de nomenclatura de URL
La base de una buena API REST comienza con URLs bien diseñadas. Tus endpoints deben ser predecibles, legibles y seguir patrones consistentes.
Usa sustantivos, no verbos
Los recursos REST deben identificarse mediante sustantivos. El método HTTP ya indica la acción.
# Good - nouns as resources
GET /users # Get all users
GET /users/123 # Get user 123
POST /users # Create a new user
DELETE /users/123 # Delete user 123
# Bad - verbs in URLs
GET /getUsers
POST /createUser
DELETE /deleteUser/123Usa plurales para colecciones
Siempre usa sustantivos en plural para endpoints de colección. Esto mantiene la consistencia.
# Good - consistent plurals
GET /users # Collection
GET /users/123 # Single resource in collection
GET /products # Collection
GET /products/456 # Single resource
# Bad - inconsistent or singular
GET /user # Ambiguous: one user or all users?
GET /user/123
GET /product-listRecursos anidados para relaciones
Usa el anidamiento para expresar relaciones entre recursos, pero evita ir más allá de dos niveles.
# Good - clear parent-child relationship (max 2 levels)
GET /users/123/orders # Orders belonging to user 123
GET /users/123/orders/456 # Specific order of user 123
# Bad - too deeply nested
GET /users/123/orders/456/items/789/reviews
# Better: use a direct resource endpoint
GET /order-items/789/reviews
# Or: flatten with query parameters
GET /reviews?order_item_id=789Patrones de URL: Buenos vs Malos
| Bueno | Malo | Razón |
|---|---|---|
| GET /users | GET /getUsers | HTTP method already implies the action |
| GET /users/123 | GET /user?id=123 | Use path parameters for resource identity |
| POST /users | POST /createUser | POST already means create |
| PUT /users/123 | POST /updateUser | PUT/PATCH for updates, use resource path |
| DELETE /users/123 | POST /deleteUser | Use DELETE method, not POST with verb |
| GET /users/123/orders | GET /getUserOrders?userId=123 | Use nested resources for relationships |
| GET /products?category=electronics | GET /electronics/products | Use query params for filtering |
| GET /users?status=active&sort=name | GET /active-users-sorted-by-name | Use query params, not encoded URLs |
| PATCH /users/123 | POST /users/123/update | Use HTTP methods, not URL verbs |
| GET /users/123/avatar | GET /getAvatarForUser/123 | Keep it resource-oriented |
Métodos HTTP
Cada método HTTP tiene semántica específica. Usarlos correctamente hace tu API predecible.
| Método | Propósito | Idempotente | Cuerpo de solicitud | Ejemplo |
|---|---|---|---|---|
| GET | Retrieve a resource or collection | Yes | No | GET /users/123 |
| POST | Create a new resource | No | Yes | POST /users |
| PUT | Replace a resource entirely | Yes | Yes | PUT /users/123 |
| PATCH | Partially update a resource | No* | Yes | PATCH /users/123 |
| DELETE | Remove a resource | Yes | Optional | DELETE /users/123 |
* PATCH can be idempotent depending on implementation. If the patch describes the final state (e.g., {"name": "John"}), it is idempotent. If it describes an operation (e.g., {"op": "increment", "path": "/count"}), it is not.
Códigos de estado HTTP
Usar los códigos de estado correctos ayuda a los clientes a manejar las respuestas apropiadamente.
| Código | Nombre | Cuándo usar |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, or DELETE. Return the resource or confirmation. |
| 201 | Created | Successful POST that created a new resource. Include Location header with the new resource URL. |
| 204 | No Content | Successful DELETE or PUT/PATCH when no response body is needed. |
| 400 | Bad Request | The request is malformed — invalid JSON, missing required fields, wrong data types. |
| 401 | Unauthorized | No valid authentication credentials provided. The client must authenticate first. |
| 403 | Forbidden | The client is authenticated but does not have permission for this action. |
| 404 | Not Found | The requested resource does not exist. Also use for hidden resources (instead of 403) for security. |
| 409 | Conflict | The request conflicts with the current state — duplicate email, version mismatch, etc. |
| 422 | Unprocessable Entity | The request is well-formed but fails validation — email format invalid, password too short. |
| 429 | Too Many Requests | Rate limit exceeded. Include Retry-After header with seconds until the client can retry. |
| 500 | Internal Server Error | An unexpected server error occurred. Log the details server-side but do not expose them to clients. |
| 503 | Service Unavailable | The server is temporarily unavailable — maintenance mode, overloaded. Include Retry-After header. |
Manejo de errores
Las respuestas de error consistentes hacen tu API más fácil de consumir. Cada error debe devolver un cuerpo JSON estructurado.
Formato estándar de respuesta de error
Usa un sobre de error consistente para todas las respuestas de error.
// Standard error response
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The user with ID 123 was not found.",
"status": 404,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_abc123def456",
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
}
}
// Another example: authentication error
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "The access token has expired. Please refresh your token.",
"status": 401,
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_xyz789"
}
}Errores de validación a nivel de campo
Para errores de validación 422, incluye información de error a nivel de campo.
// 422 Validation error with field-level details
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid fields.",
"status": 422,
"details": [
{
"field": "email",
"message": "Must be a valid email address.",
"rejected_value": "not-an-email"
},
{
"field": "password",
"message": "Must be at least 8 characters long.",
"rejected_value": "short"
},
{
"field": "age",
"message": "Must be a positive integer.",
"rejected_value": -5
}
],
"timestamp": "2026-01-15T10:30:00Z",
"request_id": "req_val456"
}
}Autenticación y autorización
Elegir la estrategia de autenticación correcta depende de tu caso de uso.
Claves API
Tokens de cadena simples pasados en encabezados o parámetros de consulta. Ideales para comunicación servidor a servidor.
# API Key in header (recommended)
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456ghi789
# API Key in query parameter (less secure - visible in logs)
GET /api/users?api_key=sk_live_abc123def456ghi789Tokens JWT Bearer
Tokens autocontenidos que llevan reclamaciones del usuario. Ideales para autenticación sin estado en microservicios.
# JWT Bearer Token in Authorization header
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
# Token refresh flow
POST /api/auth/refresh HTTP/1.1
Content-Type: application/json
{
"refresh_token": "rt_abc123def456"
}
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_new789xyz"
}OAuth 2.0
El estándar de la industria para autorización delegada. Permite a aplicaciones de terceros acceder a recursos en nombre de un usuario.
# OAuth 2.0 Authorization Code Flow
# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
response_type=code&
client_id=your_client_id&
redirect_uri=https://yourapp.com/callback&
scope=read:users write:users&
state=random_csrf_token
# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback
# Step 3: Use the access token
GET /api/users/me HTTP/1.1
Authorization: Bearer ACCESS_TOKEN_HEREComparación de autenticación
| Característica | API Key | JWT | OAuth 2.0 |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Stateless | Yes | Yes | Depends |
| User context | No | Yes | Yes |
| Token expiration | Manual rotation | Built-in (exp claim) | Built-in |
| Scope/permissions | Limited | Via claims | Fine-grained scopes |
| Third-party access | No | No | Yes (delegated) |
| Best for | Server-to-server | SPAs, Mobile apps | Third-party integrations |
| Revocation | Delete key | Blacklist needed | Revoke at auth server |
Patrones de paginación
Cualquier endpoint que devuelva una lista de recursos debe soportar paginación.
Paginación basada en offset
El enfoque más simple. Usa número de página y parámetros de límite.
# Offset-based pagination request
GET /api/users?page=2&limit=20
# How it works internally
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20; -- Skip first 20, return next 20
# Pros: Simple, allows jumping to any page
# Cons: Inconsistent with concurrent modifications,
# slow on large offsets (OFFSET 100000)Paginación basada en cursor
Usa un token cursor opaco para marcar la posición en el conjunto de datos.
# Cursor-based pagination request
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
# The cursor is an opaque token (often base64-encoded)
# that points to a specific position in the dataset
# How it works internally (cursor decodes to {"id": 123})
SELECT * FROM users
WHERE id > 123
ORDER BY id ASC
LIMIT 20;
# Pros: Consistent results, fast on large datasets
# Cons: Cannot jump to arbitrary pages,
# only next/previous navigationFormato de respuesta de paginación
Incluye metadatos sobre el estado de paginación en cada respuesta paginada.
// Offset-based pagination response
{
"data": [
{ "id": 21, "name": "Alice" },
{ "id": 22, "name": "Bob" }
// ... 18 more items
],
"meta": {
"current_page": 2,
"per_page": 20,
"total_pages": 15,
"total_count": 294
},
"links": {
"self": "/api/users?page=2&limit=20",
"first": "/api/users?page=1&limit=20",
"prev": "/api/users?page=1&limit=20",
"next": "/api/users?page=3&limit=20",
"last": "/api/users?page=15&limit=20"
}
}
// Cursor-based pagination response
{
"data": [
{ "id": 124, "name": "Charlie" },
{ "id": 125, "name": "Diana" }
// ... 18 more items
],
"meta": {
"has_more": true,
"next_cursor": "eyJpZCI6MTQzfQ",
"prev_cursor": "eyJpZCI6MTI0fQ"
},
"links": {
"next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
"prev": "/api/users?cursor=eyJpZCI6MTI0fQ&limit=20&direction=prev"
}
}Estrategias de versionado de API
Las APIs evolucionan con el tiempo. El versionado te permite hacer cambios incompatibles sin interrumpir los clientes existentes.
| Estrategia | Ejemplo | Ventajas | Desventajas |
|---|---|---|---|
| URL Path | /v1/users, /v2/users | Explicit, easy to route and cache, clear in logs and docs | URL pollution, harder to sunset old versions |
| Custom Header | Accept-Version: v2 or X-API-Version: 2 | Clean URLs, version hidden from casual users | Harder to test (need tools to set headers), not cacheable by default |
| Query Parameter | /users?version=2 | Easy to test in browser, no URL path changes | Can be accidentally omitted, harder to route, pollutes query string |
| Content Negotiation | Accept: application/vnd.api.v2+json | Most RESTful approach, per-resource versioning | Complex, hard to test, poorly supported by many tools |
# URL Path versioning (recommended)
GET /v1/users/123 # Version 1
GET /v2/users/123 # Version 2 with breaking changes
# Express.js router example
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);
app.use('/v1', v1Router);
app.use('/v2', v2Router);Limitación de velocidad
La limitación de velocidad protege tu API del abuso y asegura un uso justo.
Encabezados de limitación de velocidad
Incluye estos encabezados en cada respuesta para que los clientes puedan monitorear su uso.
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 847 # Remaining requests in window
X-RateLimit-Reset: 1706176800 # Unix timestamp when window resets
X-RateLimit-Window: 3600 # Window duration in seconds (1 hour)
# Some APIs also include:
RateLimit-Policy: 1000;w=3600 # IETF draft standard formatRespuesta 429 Demasiadas solicitudes
Cuando un cliente excede el límite, devuelve un estado 429 con encabezado Retry-After.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706176800
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 1000 requests per hour.",
"status": 429,
"retry_after": 45,
"documentation_url": "https://api.example.com/docs/rate-limiting"
}
}Estrategias de backoff
Los clientes deben implementar backoff exponencial cuando reciben respuestas 429.
// Exponential backoff with jitter
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Use Retry-After header if available
const retryAfter = response.headers.get('Retry-After');
let delay;
if (retryAfter) {
delay = parseInt(retryAfter, 10) * 1000;
} else {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const baseDelay = Math.pow(2, attempt) * 1000;
// Add jitter (random 0-1s) to prevent thundering herd
const jitter = Math.random() * 1000;
delay = baseDelay + jitter;
}
console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}Encabezados de seguridad
Los encabezados de seguridad protegen tu API de vulnerabilidades web comunes.
| Encabezado | Valor recomendado | Propósito |
|---|---|---|
| Access-Control-Allow-Origin | https://yourapp.com (specific origin) | CORS: Controls which domains can call your API. Never use * in production with credentials. |
| Access-Control-Allow-Methods | GET, POST, PUT, PATCH, DELETE | CORS: Specifies which HTTP methods are allowed for cross-origin requests. |
| Access-Control-Allow-Headers | Authorization, Content-Type | CORS: Specifies which request headers are allowed in cross-origin requests. |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | HSTS: Forces browsers to use HTTPS for all future requests to your domain. |
| Content-Security-Policy | default-src 'none'; frame-ancestors 'none' | CSP: Prevents XSS and data injection attacks. For APIs, restrict everything. |
| X-Content-Type-Options | nosniff | Prevents browsers from MIME-type sniffing. Ensures responses are treated as their declared content type. |
| X-Frame-Options | DENY | Prevents your API responses from being embedded in iframes (clickjacking protection). |
| Cache-Control | no-store, no-cache, must-revalidate | Prevents caching of sensitive API responses. Adjust per endpoint as needed. |
// Express.js security headers middleware
const helmet = require('helmet');
app.use(helmet());
// Custom CORS configuration
const cors = require('cors');
app.use(cors({
origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Cache-Control', 'no-store');
next();
});Documentación API con OpenAPI
Una buena documentación es esencial para la adopción de la API. OpenAPI es el estándar para describir APIs REST.
- Escribe primero la especificación OpenAPI (enfoque design-first), luego implementa la API.
- Incluye descripciones detalladas para cada endpoint, parámetro y esquema de respuesta.
- Proporciona valores de ejemplo realistas para todos los cuerpos de solicitud y respuesta.
- Documenta las respuestas de error para cada endpoint.
- Usa etiquetas para agrupar endpoints relacionados.
- Versiona tu especificación OpenAPI junto con tu código API.
- Configura documentación interactiva (Swagger UI o Redoc).
# OpenAPI 3.1 specification example
openapi: 3.1.0
info:
title: User Management API
version: 1.0.0
description: API for managing users and their resources
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: A paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
example:
data:
- id: 1
name: "Alice Johnson"
email: "alice@example.com"
meta:
current_page: 1
total_pages: 10
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a new user
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
example:
name: "Bob Smith"
email: "bob@example.com"
password: "securePassword123"
responses:
'201':
description: User created successfully
'422':
$ref: '#/components/responses/ValidationError'Errores comunes de REST API
Incluso los desarrolladores experimentados cometen estos errores. Revisa esta lista para asegurarte de que tu API sigue las mejores prácticas.
| # | Error | Por qué está mal | Solución |
|---|---|---|---|
| 1 | Using verbs in URLs | REST is resource-oriented; HTTP methods express actions | Use nouns: /users not /getUsers |
| 2 | Returning 200 for errors | Clients rely on status codes for error handling | Use proper status codes: 400, 404, 422, 500 |
| 3 | Not paginating list endpoints | Returns grow unbounded, causing timeouts and memory issues | Always paginate with sensible defaults (limit=20) |
| 4 | Exposing internal errors | Stack traces reveal implementation details to attackers | Log details server-side, return generic error messages |
| 5 | Ignoring CORS headers | Browser-based clients cannot call your API | Configure CORS for allowed origins and methods |
| 6 | Not versioning the API | Breaking changes affect all clients simultaneously | Use URL path versioning: /v1/users |
| 7 | Using POST for everything | Loses HTTP method semantics, breaks caching and idempotency | Use GET, POST, PUT, PATCH, DELETE appropriately |
| 8 | Inconsistent naming conventions | Mixed case or formats confuse API consumers | Pick one (snake_case or camelCase) and be consistent |
| 9 | No rate limiting | API is vulnerable to abuse and DDoS attacks | Implement rate limiting with clear headers |
| 10 | Missing Content-Type header | Clients cannot parse responses correctly | Always set Content-Type: application/json |
Prueba estas herramientas de desarrollo relacionadas
Preguntas frecuentes
¿Cuál es la diferencia entre REST y RESTful?
REST es un estilo arquitectónico definido por Roy Fielding. RESTful es un adjetivo que describe APIs que adhieren a las restricciones REST. En la práctica, los términos se usan indistintamente.
¿Debo usar PUT o PATCH para actualizar recursos?
Usa PUT cuando el cliente envía un reemplazo completo del recurso. Usa PATCH para actualizaciones parciales. PATCH es más eficiente en ancho de banda.
¿Cómo debo manejar el versionado de API?
El versionado por ruta URL (ej: /v1/users) es el enfoque más común y explícito. Es fácil de entender, enrutar y cachear.
¿Cuál es la mejor forma de manejar la autenticación en APIs REST?
Para la mayoría de aplicaciones, los tokens JWT Bearer ofrecen un buen equilibrio entre seguridad y simplicidad. Usa tokens de acceso de corta duración con rotación de tokens de refresco.
¿Cómo diseño recursos anidados en APIs REST?
Usa el anidamiento para mostrar relaciones padre-hijo: /users/123/orders. Limita el anidamiento a dos niveles máximo.
¿Las respuestas de API REST deben incluir campos null u omitirlos?
Depende de tu contrato. Incluir campos null hace el esquema predecible. Omitirlos reduce el tamaño del payload. Sé consistente y documéntalo.