APIs RESTful

REST, HATEOAS, serialización, filtrado, AJAX, CSRF, CORS y desarrollo de APIs modernas

Objetivos de Aprendizaje

Introducción a REST

¿Qué es REST?

REST (Representational State Transfer) es un estilo arquitectónico para diseñar servicios web que utiliza HTTP de manera eficiente, proporcionando una interfaz uniforme para la comunicación entre sistemas.

Principios Fundamentales de REST

Cliente-Servidor

Separación clara entre cliente y servidor

Sin Estado

Cada petición contiene toda la información necesaria

Cacheable

Las respuestas pueden ser cacheadas

Interfaz Uniforme

Uso consistente de métodos HTTP

Sistema en Capas

Arquitectura modular y escalable

Código bajo Demanda

Capacidad de enviar código ejecutable

Métodos HTTP en REST

GET

Obtener recursos

GET /api/usuarios/ Obtener lista de usuarios

POST

Crear recursos

POST /api/usuarios/ Crear nuevo usuario

PUT

Actualizar completo

PUT /api/usuarios/1/ Actualizar usuario completo

PATCH

Actualizar parcial

PATCH /api/usuarios/1/ Actualizar campos específicos

DELETE

Eliminar recursos

DELETE /api/usuarios/1/ Eliminar usuario

HATEOAS

Hypermedia as the Engine of Application State

HATEOAS es una restricción de REST que permite que los clientes descubran dinámicamente las acciones disponibles a través de hipermedia, haciendo las APIs más autodescriptivas.

Descubrimiento Dinámico

Los clientes pueden descubrir acciones disponibles automáticamente

Evolución de API

Las APIs pueden evolucionar sin romper clientes existentes

Bajo Acoplamiento

Reduce la dependencia entre cliente y servidor

Ejemplo de Respuesta HATEOAS

{
  "id": 1,
  "nombre": "Juan Pérez",
  "email": "juan@ejemplo.com",
  "estado": "activo",
  "_links": {
    "self": {
      "href": "/api/usuarios/1/",
      "method": "GET"
    },
    "edit": {
      "href": "/api/usuarios/1/",
      "method": "PUT",
      "title": "Editar usuario"
    },
    "delete": {
      "href": "/api/usuarios/1/",
      "method": "DELETE",
      "title": "Eliminar usuario"
    },
    "posts": {
      "href": "/api/usuarios/1/posts/",
      "method": "GET",
      "title": "Posts del usuario"
    },
    "avatar": {
      "href": "/api/usuarios/1/avatar/",
      "method": "POST",
      "title": "Subir avatar",
      "type": "multipart/form-data"
    }
  },
  "_embedded": {
    "perfil": {
      "bio": "Desarrollador Full Stack",
      "ubicacion": "Madrid, España",
      "_links": {
        "self": {
          "href": "/api/perfiles/1/"
        }
      }
    }
  }
}

Serialización y Filtrado

Serialización
Filtrado
Paginación

Serialización de Datos

La serialización convierte objetos Python en formatos como JSON para transmisión HTTP, mientras que la deserialización hace el proceso inverso.

Serializer Básico

# serializers.py
from rest_framework import serializers
from .models import Usuario, Post

class UsuarioSerializer(serializers.ModelSerializer):
    posts_count = serializers.SerializerMethodField()
    avatar_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Usuario
        fields = ['id', 'username', 'email', 'first_name', 'last_name', 
                 'fecha_registro', 'posts_count', 'avatar_url']
        read_only_fields = ['id', 'fecha_registro']
    
    def get_posts_count(self, obj):
        return obj.posts.count()
    
    def get_avatar_url(self, obj):
        if obj.avatar:
            request = self.context.get('request')
            if request:
                return request.build_absolute_uri(obj.avatar.url)
        return None
    
    def validate_email(self, value):
        if Usuario.objects.filter(email=value).exists():
            raise serializers.ValidationError("Este email ya está en uso")
        return value

class PostSerializer(serializers.ModelSerializer):
    autor = UsuarioSerializer(read_only=True)
    comentarios_count = serializers.SerializerMethodField()
    
    class Meta:
        model = Post
        fields = ['id', 'titulo', 'contenido', 'autor', 'fecha_creacion',
                 'fecha_actualizacion', 'comentarios_count']
    
    def get_comentarios_count(self, obj):
        return obj.comentarios.count()

Serializer Anidado

# Serializers con relaciones anidadas
class ComentarioSerializer(serializers.ModelSerializer):
    autor = serializers.StringRelatedField()
    
    class Meta:
        model = Comentario
        fields = ['id', 'contenido', 'autor', 'fecha_creacion']

class PostDetalleSerializer(serializers.ModelSerializer):
    autor = UsuarioSerializer(read_only=True)
    comentarios = ComentarioSerializer(many=True, read_only=True)
    etiquetas = serializers.StringRelatedField(many=True)
    
    class Meta:
        model = Post
        fields = ['id', 'titulo', 'contenido', 'autor', 'fecha_creacion',
                 'fecha_actualizacion', 'comentarios', 'etiquetas']
    
    def create(self, validated_data):
        validated_data['autor'] = self.context['request'].user
        return super().create(validated_data)

Filtrado de Datos

El filtrado permite a los clientes especificar criterios para obtener subconjuntos específicos de datos.

Filtros con django-filter

# filters.py
import django_filters
from .models import Post, Usuario

class PostFilter(django_filters.FilterSet):
    titulo = django_filters.CharFilter(lookup_expr='icontains')
    autor = django_filters.ModelChoiceFilter(queryset=Usuario.objects.all())
    fecha_desde = django_filters.DateFilter(field_name='fecha_creacion', lookup_expr='gte')
    fecha_hasta = django_filters.DateFilter(field_name='fecha_creacion', lookup_expr='lte')
    contenido = django_filters.CharFilter(lookup_expr='icontains')
    
    class Meta:
        model = Post
        fields = ['titulo', 'autor', 'fecha_desde', 'fecha_hasta', 'contenido']

# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_class = PostFilter
    search_fields = ['titulo', 'contenido', 'autor__username']
    ordering_fields = ['fecha_creacion', 'titulo']
    ordering = ['-fecha_creacion']

Filtros Personalizados

# Filtros personalizados en ViewSets
class PostViewSet(viewsets.ModelViewSet):
    serializer_class = PostSerializer
    
    def get_queryset(self):
        queryset = Post.objects.all()
        
        # Filtro por autor
        autor_id = self.request.query_params.get('autor', None)
        if autor_id:
            queryset = queryset.filter(autor_id=autor_id)
        
        # Filtro por popularidad
        popular = self.request.query_params.get('popular', None)
        if popular == 'true':
            queryset = queryset.annotate(
                comentarios_count=Count('comentarios')
            ).filter(comentarios_count__gte=5)
        
        # Filtro por rango de fechas
        fecha_desde = self.request.query_params.get('fecha_desde', None)
        fecha_hasta = self.request.query_params.get('fecha_hasta', None)
        
        if fecha_desde:
            queryset = queryset.filter(fecha_creacion__gte=fecha_desde)
        if fecha_hasta:
            queryset = queryset.filter(fecha_creacion__lte=fecha_hasta)
        
        return queryset.select_related('autor').prefetch_related('comentarios')

Comunicación AJAX

AJAX en Aplicaciones Modernas

AJAX (Asynchronous JavaScript and XML) permite comunicación asíncrona con el servidor sin recargar la página, creando experiencias de usuario más fluidas.

Fetch API

API moderna nativa del navegador

Pros: Nativo, promesas, moderno
Contras: No soporta IE

Axios

Librería popular para HTTP

Pros: Interceptores, transformaciones
Contras: Dependencia externa

jQuery AJAX

Método tradicional con jQuery

Pros: Amplio soporte, simple
Contras: Dependencia de jQuery

Fetch API

// GET Request
async function obtenerUsuarios() {
    try {
        const response = await fetch('/api/usuarios/', {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error:', error);
        throw error;
    }
}

// POST Request
async function crearUsuario(userData) {
    try {
        const response = await fetch('/api/usuarios/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('token')}`,
                'X-CSRFToken': getCookie('csrftoken')
            },
            body: JSON.stringify(userData)
        });
        
        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'Error al crear usuario');
        }
        
        return await response.json();
    } catch (error) {
        console.error('Error:', error);
        throw error;
    }
}

Axios con Interceptores

// Configuración de Axios
const api = axios.create({
    baseURL: '/api/',
    timeout: 10000,
    headers: {
        'Content-Type': 'application/json'
    }
});

// Interceptor de request
api.interceptors.request.use(
    config => {
        const token = localStorage.getItem('token');
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        
        // Agregar CSRF token para métodos que lo requieren
        if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
            config.headers['X-CSRFToken'] = getCookie('csrftoken');
        }
        
        return config;
    },
    error => Promise.reject(error)
);

// Interceptor de response
api.interceptors.response.use(
    response => response,
    error => {
        if (error.response?.status === 401) {
            // Token expirado, redirigir a login
            localStorage.removeItem('token');
            window.location.href = '/login/';
        }
        return Promise.reject(error);
    }
);

// Uso de la API
class UsuarioService {
    static async obtenerTodos() {
        const response = await api.get('usuarios/');
        return response.data;
    }
    
    static async crear(userData) {
        const response = await api.post('usuarios/', userData);
        return response.data;
    }
    
    static async actualizar(id, userData) {
        const response = await api.put(`usuarios/${id}/`, userData);
        return response.data;
    }
    
    static async eliminar(id) {
        await api.delete(`usuarios/${id}/`);
    }
}

CSRF y CORS

CSRF (Cross-Site Request Forgery)

Protección contra ataques que ejecutan acciones no autorizadas en nombre del usuario.

Implementación en Django

# settings.py
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    # ... otros middlewares
]

# En templates
{% csrf_token %}

# En JavaScript
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

// Uso en AJAX
fetch('/api/endpoint/', {
    method: 'POST',
    headers: {
        'X-CSRFToken': getCookie('csrftoken'),
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
});

CORS (Cross-Origin Resource Sharing)

Mecanismo que permite que recursos de una página web sean accedidos desde otro dominio.

Configuración CORS

# Instalación
pip install django-cors-headers

# settings.py
INSTALLED_APPS = [
    'corsheaders',
    # ... otras apps
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ... otros middlewares
]

# Configuración CORS
CORS_ALLOWED_ORIGINS = [
    "https://ejemplo.com",
    "https://www.ejemplo.com",
    "http://localhost:3000",
    "http://127.0.0.1:3000",
]

# Para desarrollo (NO usar en producción)
CORS_ALLOW_ALL_ORIGINS = True

# Headers permitidos
CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
]

# Métodos permitidos
CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]

Proyecto en GitHub

API RESTful Completa

Sistema de API REST completo con autenticación, filtrado, paginación y documentación automática implementando todos los conceptos de esta semana.

REST Completo

Implementación completa de principios REST con HATEOAS

Filtrado Avanzado

Sistema de filtros, búsqueda y ordenamiento dinámico

Cliente AJAX

Frontend interactivo con comunicación asíncrona

Seguridad

Protección CSRF, CORS y autenticación JWT

Endpoints Principales

GET /api/usuarios/ Lista de usuarios con filtros
POST /api/usuarios/ Crear nuevo usuario
GET /api/posts/ Posts con paginación y búsqueda
PUT /api/posts/{id}/ Actualizar post completo

Resumen Semana 13

Principios REST

Comprensión profunda de la arquitectura REST y sus principios fundamentales para APIs escalables.

HATEOAS

Implementación de hipermedia para crear APIs autodescriptivas y evolutivas.

Serialización

Dominio de serializers para transformación eficiente de datos entre formatos.

Filtrado y Paginación

Sistemas avanzados de filtrado, búsqueda y paginación para grandes conjuntos de datos.

Comunicación AJAX

Implementación de comunicación asíncrona moderna con Fetch API y Axios.

Seguridad API

Protección completa con CSRF, CORS y mejores prácticas de seguridad.