TechLead
Principiante
25 min
Lección 2 de 10
API

APIs REST

Aprende los principios de diseño de APIs RESTful, métodos HTTP, endpoints y códigos de estado

¿Qué es REST?

REST (Transferencia de Estado Representacional) es un estilo arquitectónico para diseñar aplicaciones en red. Creado por Roy Fielding en 2000, REST se ha convertido en el patrón de diseño dominante para APIs web debido a su simplicidad, escalabilidad y uso de protocolos HTTP estándar.

Una API RESTful es aquella que cumple con los principios REST, proporcionando una interfaz uniforme para interactuar con recursos.

Principios de REST

🔗 Interfaz Uniforme

Forma consistente de interactuar con recursos usando métodos HTTP y URLs estándar.

🚫 Sin Estado

Cada petición contiene toda la información necesaria. El servidor no almacena el estado del cliente.

📦 Cacheable

Las respuestas pueden ser cacheadas para mejorar el rendimiento y reducir la carga del servidor.

📊 Sistema en Capas

El cliente no puede distinguir si está conectado directamente al servidor o a través de intermediarios.

🖥️ Cliente-Servidor

Separación de responsabilidades entre cliente (UI) y servidor (datos).

📝 Basado en Recursos

Todo es un recurso identificado por URIs. Los recursos tienen representaciones (JSON/XML).

Estructura de URL RESTful

Las APIs REST usan URLs descriptivas y jerárquicas para representar recursos:

// Estructura de URL de Recurso
https://api.example.com/v1/users              // Colección de usuarios
https://api.example.com/v1/users/123          // Usuario único (ID: 123)
https://api.example.com/v1/users/123/posts    // Posts del usuario
https://api.example.com/v1/users/123/posts/456 // Post específico

// Componentes de URL:
// Protocolo: https://
// Host: api.example.com
// Versión: /v1
// Recurso: /users
// Identificador: /123
// Sub-recurso: /posts

✅ Buenas Prácticas de URL:

  • • Usar sustantivos, no verbos: /users no /getUsers
  • • Usar sustantivos en plural: /users no /user
  • • Usar minúsculas: /users no /Users
  • • Usar guiones para palabras múltiples: /user-profiles no /userProfiles

Métodos HTTP en Detalle

GET Recuperar Recurso(s)
// GET - Recuperar todos los usuarios
const response = await fetch('https://api.example.com/users');
const users = await response.json();

// GET - Recuperar un usuario único
const user = await fetch('https://api.example.com/users/123');
const userData = await user.json();

// GET con parámetros de consulta (filtrado/paginación)
const filteredUsers = await fetch(
  'https://api.example.com/users?role=admin&page=1&limit=10'
);

// Respuesta: 200 OK
{
  "data": [
    { "id": 1, "name": "Juan Pérez", "role": "admin" }
  ],
  "pagination": { "page": 1, "limit": 10, "total": 45 }
}
POST Crear Nuevo Recurso
// POST - Crear nuevo usuario
const newUser = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'Jane Smith',
    email: 'jane@example.com',
    role: 'user'
  })
});

const created = await newUser.json();

// Respuesta: 201 Created
{
  "id": 124,
  "name": "Jane Smith",
  "email": "jane@example.com",
  "role": "user",
  "createdAt": "2024-01-15T10:30:00Z"
}
PUT Reemplazar Recurso Completo
// PUT - Reemplazar usuario completo (todos los campos requeridos)
const updatedUser = await fetch('https://api.example.com/users/124', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'Jane Doe',        // Cambiado
    email: 'jane@example.com', // Igual
    role: 'admin'            // Cambiado
  })
});

// Respuesta: 200 OK
{
  "id": 124,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "admin",
  "updatedAt": "2024-01-15T11:00:00Z"
}
PATCH Actualización Parcial
// PATCH - Actualizar solo campos específicos
const patchedUser = await fetch('https://api.example.com/users/124', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    role: 'moderator'  // Solo actualizando rol
  })
});

// Respuesta: 200 OK
{
  "id": 124,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "role": "moderator",
  "updatedAt": "2024-01-15T11:30:00Z"
}
DELETE Eliminar Recurso
// DELETE - Eliminar usuario
const deleteResponse = await fetch('https://api.example.com/users/124', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer token123'
  }
});

// Respuesta: 204 No Content (cuerpo vacío)
// O
// Respuesta: 200 OK
{
  "message": "Usuario eliminado exitosamente",
  "deletedId": 124
}

Códigos de Estado HTTP

Los códigos de estado indican el resultado de una petición HTTP:

✅ 2xx Éxito

200 OK - Petición exitosa
201 Created - Recurso creado
204 No Content - Éxito, sin cuerpo
202 Accepted - Procesamiento en cola

↪️ 3xx Redirección

301 Moved Permanently - Nueva URL
302 Found - Redirección temporal
304 Not Modified - Usar caché
307 Temporary Redirect

⚠️ 4xx Errores del Cliente

400 Bad Request - Sintaxis inválida
401 Unauthorized - Autenticación requerida
403 Forbidden - Sin permiso
404 Not Found - Recurso faltante
422 Unprocessable - Error de validación
429 Too Many Requests - Limitación de tasa

❌ 5xx Errores del Servidor

500 Internal Error - Error del servidor
502 Bad Gateway - Upstream inválido
503 Unavailable - Servidor caído
504 Gateway Timeout - Tiempo de espera agotado

Cliente API REST Completo

Construyendo una clase de cliente API reutilizable:

// Clase de Cliente API REST
class ApiClient {
  constructor(baseUrl, token = null) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  setToken(token) {
    this.token = token;
  }

  getHeaders() {
    const headers = {
      'Content-Type': 'application/json',
    };
    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }
    return headers;
  }

  async request(method, endpoint, data = null) {
    const url = `${this.baseUrl}${endpoint}`;
    const options = {
      method,
      headers: this.getHeaders(),
    };

    if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
      options.body = JSON.stringify(data);
    }

    try {
      const response = await fetch(url, options);
      const contentType = response.headers.get('content-type');
      let responseData;
      
      if (contentType?.includes('application/json')) {
        responseData = await response.json();
      } else {
        responseData = await response.text();
      }

      if (!response.ok) {
        throw {
          status: response.status,
          message: responseData.message || 'Petición fallida',
          data: responseData
        };
      }

      return responseData;
    } catch (error) {
      console.error(`Error de API [${method}] ${endpoint}:`, error);
      throw error;
    }
  }

  // Métodos de conveniencia
  get(endpoint) { return this.request('GET', endpoint); }
  post(endpoint, data) { return this.request('POST', endpoint, data); }
  put(endpoint, data) { return this.request('PUT', endpoint, data); }
  patch(endpoint, data) { return this.request('PATCH', endpoint, data); }
  delete(endpoint) { return this.request('DELETE', endpoint); }
}

// Ejemplo de Uso
const api = new ApiClient('https://api.example.com/v1');
api.setToken('tu-jwt-token');

// Operaciones CRUD
async function userOperations() {
  // Crear
  const newUser = await api.post('/users', {
    name: 'Juan Pérez',
    email: 'juan@example.com'
  });

  // Leer
  const users = await api.get('/users');
  const user = await api.get(`/users/${newUser.id}`);

  // Actualizar
  const updated = await api.patch(`/users/${newUser.id}`, {
    name: 'Juan Martínez'
  });

  // Eliminar
  await api.delete(`/users/${newUser.id}`);
}

userOperations();

Parámetros de Consulta

Usa parámetros de consulta para filtrado, ordenamiento y paginación:

// Construyendo URLs con Parámetros de Consulta
class QueryBuilder {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.params = new URLSearchParams();
  }

  page(page, limit = 10) {
    this.params.set('page', page);
    this.params.set('limit', limit);
    return this;
  }

  filter(field, value) {
    this.params.set(field, value);
    return this;
  }

  sort(field, order = 'asc') {
    this.params.set('sort', field);
    this.params.set('order', order);
    return this;
  }

  search(query) {
    this.params.set('q', query);
    return this;
  }

  fields(fieldList) {
    this.params.set('fields', fieldList.join(','));
    return this;
  }

  build() {
    const queryString = this.params.toString();
    return queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
  }
}

// Uso
const url = new QueryBuilder('https://api.example.com/users')
  .page(2, 20)
  .filter('role', 'admin')
  .sort('createdAt', 'desc')
  .search('juan')
  .fields(['id', 'name', 'email'])
  .build();

console.log(url);
// https://api.example.com/users?page=2&limit=20&role=admin&sort=createdAt&order=desc&q=juan&fields=id,name,email

Estándares de Formato de Respuesta

// Estructura de Respuesta API Consistente

// Respuesta de Éxito
{
  "success": true,
  "data": {
    "id": 123,
    "name": "Juan Pérez",
    "email": "juan@example.com"
  },
  "meta": {
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

// Respuesta de Colección con Paginación
{
  "success": true,
  "data": [
    { "id": 1, "name": "Juan" },
    { "id": 2, "name": "María" }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100,
    "totalPages": 10,
    "hasNext": true,
    "hasPrev": false
  }
}

// Respuesta de Error
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Datos de entrada inválidos",
    "details": [
      { "field": "email", "message": "Formato de email inválido" },
      { "field": "age", "message": "Debe ser un número positivo" }
    ]
  }
}

💡 Mejores Prácticas de API REST

  • Usa métodos HTTP apropiados - GET para leer, POST para crear, etc.
  • Retorna códigos de estado apropiados - 201 para creado, 404 para no encontrado
  • Versiona tu API - Usa /v1/, /v2/ en URLs
  • Usa JSON para datos - Formato estándar para APIs web
  • Implementa paginación - No retornes resultados ilimitados
  • Documenta tu API - Usa OpenAPI/Swagger