Principios de Diseno de REST API - Diseno de API Escalable y Mantenible

Intermedio | 2025.12.02

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

MetodoUsoIdempotenciaSeguridad
GETObtener recurso
POSTCrear recurso××
PUTReemplazar recurso completamente×
PATCHActualizar recurso parcialmente××
DELETEEliminar recurso×
HEADObtener metadatos
OPTIONSVerificar 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:

CodigoDescripcion
200 OKExito (GET, PUT, PATCH)
201 CreatedCreacion exitosa (POST)
204 No ContentExito, sin cuerpo de respuesta (DELETE)

3xx Redireccion:

CodigoDescripcion
301 Moved PermanentlyMovimiento permanente
304 Not ModifiedCache valida

4xx Error del Cliente:

CodigoDescripcion
400 Bad RequestSolicitud invalida
401 UnauthorizedAutenticacion requerida
403 ForbiddenSin permisos de acceso
404 Not FoundRecurso no existe
405 Method Not AllowedMetodo no permitido
409 ConflictConflicto de recursos
422 Unprocessable EntityError de validacion
429 Too Many RequestsLimite de tasa excedido

5xx Error del Servidor:

CodigoDescripcion
500 Internal Server ErrorError interno
502 Bad GatewayError de gateway
503 Service UnavailableServicio no disponible
504 Gateway TimeoutTiempo 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

MetodoEjemploVentajasDesventajas
Basado en offsetGET /users?limit=10&offset=20Simple, salto a cualquier paginaLento con grandes datos, duplicados/omisiones al agregar datos
Basado en cursorGET /users?limit=10&cursor=eyJpZCI6MTAwfQRapido, resistente a cambios de datosNo puede saltar a paginas arbitrarias
Basado en keysetGET /users?limit=10&after_id=100Simple y rapidoDepende 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

MetodoEjemploVentajasDesventajas
Ruta URL/api/v1/usersClaro, facil de cachearRequiere cambio de URL
Parametro de consulta/api/users?version=1FlexibleDificil de cachear
EncabezadoAccept: application/vnd.api+json;version=1URL limpiaDificil de descubrir
Tipo de medioAccept: application/vnd.myapi.v1+jsonEstandarComplejo
// 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

← Volver a la lista