Principios Basicos de REST API
REST (Representational State Transfer) es un estilo arquitectonico para el diseno de servicios web. Se disena basandose en 6 restricciones.
flowchart TB
subgraph REST["6 Restricciones de REST"]
CS["1. Client-Server<br/>Separacion cliente-servidor"]
SL["2. Stateless<br/>Cada solicitud es independiente"]
CA["3. Cacheable<br/>Indicar si es cacheable"]
UI["4. Uniform Interface<br/>Interfaz uniforme"]
LS["5. Layered System<br/>Sistema en capas"]
CD["6. Code on Demand<br/>(Opcional)"]
end
Client["Client<br/>(UI)"] <-->|HTTP| Server["Server<br/>(Data)"]
Diseno de Recursos
Convenciones de Nomenclatura
Buenos ejemplos:
/users- Plural, sustantivo/users/123- ID del recurso/users/123/orders- Recursos anidados/users/123/orders/456- Subrecurso especifico
Malos ejemplos:
/getUsers- No usar verbos/user- Evitar singular/Users- Evitar mayusculas/user-list- Expresion de lista innecesaria/api/v1/get-all-users- Verbo + expresion redundante
Relaciones jerarquicas:
/organizations/{orgId}
/organizations/{orgId}/teams/{teamId}
/organizations/{orgId}/teams/{teamId}/members
Evitar anidamiento profundo (maximo 3 niveles):
- ❌
/organizations/{id}/teams/{id}/projects/{id}/tasks - ✅
/tasks?projectId={id}
Colecciones y Documentos
Estructura de recursos:
/users → Coleccion (User[])
/users/123 → Documento (User)
/users/123/avatar → Subrecurso (unico)
/users/123/orders → Subcoleccion (Order[])
/users/123/orders/456 → Subdocumento (Order)
Uso de Metodos HTTP
| Metodo | Uso | Idempotencia | Seguridad |
|---|---|---|---|
| GET | Obtener recurso | ○ | ○ |
| POST | Crear recurso | × | × |
| PUT | Reemplazar recurso completamente | ○ | × |
| PATCH | Actualizar recurso parcialmente | × | × |
| DELETE | Eliminar recurso | ○ | × |
| HEAD | Obtener metadatos | ○ | ○ |
| OPTIONS | Verificar metodos disponibles | ○ | ○ |
// Ejemplo de implementacion en Express.js
import express from 'express';
const router = express.Router();
// GET: Obtener recurso
router.get('/users', async (req, res) => {
const users = await userService.findAll(req.query);
res.json({ data: users });
});
router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
});
// POST: Crear recurso
router.post('/users', async (req, res) => {
const user = await userService.create(req.body);
res.status(201).json({ data: user });
});
// PUT: Reemplazar recurso completamente
router.put('/users/:id', async (req, res) => {
const user = await userService.replace(req.params.id, req.body);
res.json({ data: user });
});
// PATCH: Actualizar recurso parcialmente
router.patch('/users/:id', async (req, res) => {
const user = await userService.update(req.params.id, req.body);
res.json({ data: user });
});
// DELETE: Eliminar recurso
router.delete('/users/:id', async (req, res) => {
await userService.delete(req.params.id);
res.status(204).send();
});
Codigos de Estado HTTP
Lista de Codigos de Estado
2xx Exito:
| Codigo | Descripcion |
|---|---|
| 200 OK | Exito (GET, PUT, PATCH) |
| 201 Created | Creacion exitosa (POST) |
| 204 No Content | Exito, sin cuerpo de respuesta (DELETE) |
3xx Redireccion:
| Codigo | Descripcion |
|---|---|
| 301 Moved Permanently | Movimiento permanente |
| 304 Not Modified | Cache valida |
4xx Error del Cliente:
| Codigo | Descripcion |
|---|---|
| 400 Bad Request | Solicitud invalida |
| 401 Unauthorized | Autenticacion requerida |
| 403 Forbidden | Sin permisos de acceso |
| 404 Not Found | Recurso no existe |
| 405 Method Not Allowed | Metodo no permitido |
| 409 Conflict | Conflicto de recursos |
| 422 Unprocessable Entity | Error de validacion |
| 429 Too Many Requests | Limite de tasa excedido |
5xx Error del Servidor:
| Codigo | Descripcion |
|---|---|
| 500 Internal Server Error | Error interno |
| 502 Bad Gateway | Error de gateway |
| 503 Service Unavailable | Servicio no disponible |
| 504 Gateway Timeout | Tiempo de espera agotado |
Diseno de Respuestas de Error
// Formato de respuesta de error unificado
interface ErrorResponse {
error: {
code: string; // Codigo de error legible por maquina
message: string; // Mensaje legible por humanos
details?: ErrorDetail[]; // Informacion detallada (errores de validacion, etc.)
requestId?: string; // ID de solicitud para depuracion
documentation?: string; // Enlace a documentacion
};
}
interface ErrorDetail {
field: string;
message: string;
code: string;
}
// Ejemplo de implementacion
class ApiError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: ErrorDetail[]
) {
super(message);
}
}
// Middleware de manejo de errores
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
const requestId = req.headers['x-request-id'] || generateRequestId();
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
requestId,
},
});
}
// Error inesperado
console.error(`[${requestId}] Unexpected error:`, err);
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
requestId,
},
});
}
// Ejemplo de error de validacion
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"code": "TOO_SHORT"
}
],
"requestId": "req_abc123"
}
}
Paginacion
| Metodo | Ejemplo | Ventajas | Desventajas |
|---|---|---|---|
| Basado en offset | GET /users?limit=10&offset=20 | Simple, salto a cualquier pagina | Lento con grandes datos, duplicados/omisiones al agregar datos |
| Basado en cursor | GET /users?limit=10&cursor=eyJpZCI6MTAwfQ | Rapido, resistente a cambios de datos | No puede saltar a paginas arbitrarias |
| Basado en keyset | GET /users?limit=10&after_id=100 | Simple y rapido | Depende del orden de clasificacion |
Recomendado: Datos en tiempo real → Cursor / Datos estaticos → Offset
Ejemplo de Implementacion
// Paginacion basada en cursor
interface PaginatedResponse<T> {
data: T[];
pagination: {
cursor: string | null;
hasMore: boolean;
totalCount?: number;
};
}
async function getUsers(cursor?: string, limit = 10): Promise<PaginatedResponse<User>> {
let query = db.users.orderBy('createdAt', 'desc');
if (cursor) {
const decoded = decodeCursor(cursor);
query = query.where('createdAt', '<', decoded.createdAt);
}
const users = await query.limit(limit + 1).exec();
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return {
data,
pagination: {
cursor: data.length > 0 ? encodeCursor(data[data.length - 1]) : null,
hasMore,
},
};
}
function encodeCursor(user: User): string {
return Buffer.from(JSON.stringify({ id: user.id, createdAt: user.createdAt })).toString('base64');
}
function decodeCursor(cursor: string): { id: string; createdAt: Date } {
return JSON.parse(Buffer.from(cursor, 'base64').toString());
}
Versionado
| Metodo | Ejemplo | Ventajas | Desventajas |
|---|---|---|---|
| Ruta URL | /api/v1/users | Claro, facil de cachear | Requiere cambio de URL |
| Parametro de consulta | /api/users?version=1 | Flexible | Dificil de cachear |
| Encabezado | Accept: application/vnd.api+json;version=1 | URL limpia | Dificil de descubrir |
| Tipo de medio | Accept: application/vnd.myapi.v1+json | Estandar | Complejo |
// Metodo de ruta URL (recomendado)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Metodo de encabezado
app.use((req, res, next) => {
const version = req.headers['api-version'] || '1';
req.apiVersion = parseInt(version);
next();
});
Filtrado, Ordenacion y Busqueda
# Filtrado
GET /users?status=active&role=admin
GET /orders?created_after=2024-01-01&created_before=2024-12-31
GET /products?price_min=100&price_max=500
# Ordenacion
GET /users?sort=created_at # Ascendente
GET /users?sort=-created_at # Descendente
GET /users?sort=name,-created_at # Multiples campos
# Busqueda
GET /users?q=john # Busqueda de texto completo
GET /users?search[name]=john # Especificar campo
# Seleccion de campos
GET /users?fields=id,name,email # Solo campos necesarios
GET /users?include=orders,profile # Incluir relaciones
// Implementacion del constructor de consultas
function buildQuery(params: QueryParams) {
let query = db.users;
// Filtrado
if (params.status) {
query = query.where('status', '=', params.status);
}
// Ordenacion
if (params.sort) {
const fields = params.sort.split(',');
for (const field of fields) {
const order = field.startsWith('-') ? 'desc' : 'asc';
const column = field.replace(/^-/, '');
query = query.orderBy(column, order);
}
}
// Seleccion de campos
if (params.fields) {
const columns = params.fields.split(',');
query = query.select(columns);
}
return query;
}
Limitacion de Tasa
// Encabezados de limitacion de tasa
app.use((req, res, next) => {
const rateLimit = getRateLimit(req);
res.set({
'X-RateLimit-Limit': rateLimit.limit,
'X-RateLimit-Remaining': rateLimit.remaining,
'X-RateLimit-Reset': rateLimit.resetAt,
});
if (rateLimit.remaining <= 0) {
return res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests',
retryAfter: rateLimit.resetAt,
},
});
}
next();
});
Autenticacion y Autorizacion
// Autenticacion Bearer Token
const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Missing or invalid authorization header',
},
});
}
const token = authHeader.substring(7);
try {
const payload = await verifyToken(token);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Token is invalid or expired',
},
});
}
};
// Autorizacion basada en roles
const requireRole = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: {
code: 'FORBIDDEN',
message: 'Insufficient permissions',
},
});
}
next();
};
};
// Ejemplo de uso
router.delete('/users/:id', authMiddleware, requireRole('admin'), deleteUser);
Documentacion OpenAPI (Swagger)
# openapi.yaml
openapi: 3.0.3
info:
title: User API
version: 1.0.0
description: User management API
paths:
/users:
get:
summary: List all users
tags: [Users]
parameters:
- name: limit
in: query
schema:
type: integer
default: 10
- name: cursor
in: query
schema:
type: string
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
format: email
name:
type: string
required: [id, email, name]
Enlaces de Referencia
- RESTful Web APIs (O’Reilly)
- HTTP API Design Guide
- Microsoft REST API Guidelines
- JSON:API Specification