Para crear una API con Django REST Framework (DRF), instala Django y DRF, modela el recurso, crea un serializer, expón un ModelViewSet y regístralo en un router. Ese recorrido ofrece operaciones CRUD y una interfaz navegable con poco código. Sin embargo, una API preparada para producción necesita también validación, autenticación, permisos, paginación, pruebas y errores predecibles.

Esta guía construye una API ejecutable de tareas. Emplea SQLite en local y autenticación por token, con límites claros para sustituir esas decisiones más adelante. Si todavía no dominas el framework base, empieza por la guía completa de Django.

Preparar el proyecto

Crea un entorno aislado, instala dependencias e inicia el proyecto:

python -m venv .venv
## Linux/macOS: source .venv/bin/activate
## Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install Django djangorestframework
django-admin startproject config .
python manage.py startapp tasks

Los entornos virtuales evitan colisiones; consulta venv en Python. Registra aplicaciones y valores predeterminados en config/settings.py:

INSTALLED_APPS += [  # Conserva las aplicaciones generadas por Django.
    "rest_framework",
    "rest_framework.authtoken",
    "tasks",
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

Unos valores globales cerrados impiden que una vista nueva se publique accidentalmente. La autenticación establece quién llama; los permisos deciden qué puede hacer esa identidad. No son conceptos intercambiables.

Modelo y migraciones

Representa la propiedad explícitamente en tasks/models.py:

from django.conf import settings
from django.db import models


class Task(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="tasks",
    )
    title = models.CharField(max_length=200)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self) -> str:
        return self.title

Ejecuta python manage.py makemigrations y python manage.py migrate. La relación owner establece una frontera entre cuentas siempre que cada consulta la aplique. Para producción, evalúa PostgreSQL con Python y crea índices según consultas medidas.

Serializer y validación

El serializer convierte modelos en valores renderizables, valida entradas y controla campos escribibles. Crea tasks/serializers.py:

from rest_framework import serializers

from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ["id", "title", "completed", "created_at"]
        read_only_fields = ["id", "created_at"]

    def validate_title(self, value: str) -> str:
        title = value.strip()
        if len(title) < 3:
            raise serializers.ValidationError(
                "El título debe tener al menos 3 caracteres."
            )
        return title

No expongas owner como entrada escribible: el servidor lo obtiene de request.user. Evita fields = "__all__", porque un campo sensible añadido posteriormente al modelo podría entrar en el contrato público sin revisión. Una lista explícita funciona como documentación y lista permitida.

ViewSet, aislamiento y rutas

Crea tasks/views.py:

from rest_framework import viewsets

from .models import Task
from .serializers import TaskSerializer


class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer

    def get_queryset(self):
        return Task.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

get_queryset() impide listar y recuperar por ID registros de otra persona. Filtrar únicamente la lista dejaría abierta una vulnerabilidad de autorización por objeto. Registra las rutas en config/urls.py:

from django.contrib import admin
from django.urls import include, path
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.routers import DefaultRouter

from tasks.views import TaskViewSet

router = DefaultRouter()
router.register("tasks", TaskViewSet, basename="task")

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include(router.urls)),
    path("api/token/", obtain_auth_token, name="api-token"),
]

El router genera GET/POST /api/tasks/ y GET/PUT/PATCH/DELETE /api/tasks/{id}/. Además mantiene nombres coherentes para resolver URLs desde pruebas y clientes.

Autenticar y probar manualmente

Crea un usuario con python manage.py createsuperuser, ejecuta python manage.py runserver y solicita un token:

curl -X POST http://127.0.0.1:8000/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"clave-local"}'

curl -X POST http://127.0.0.1:8000/api/tasks/ \
  -H "Authorization: Token TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Revisar la API"}'

En PowerShell, llama curl.exe explícitamente y usa el acento grave para continuar la línea:

curl.exe -X POST http://127.0.0.1:8000/api/token/ `
  -H "Content-Type: application/json" `
  -d '{"username":"admin","password":"clave-local"}'

curl.exe -X POST http://127.0.0.1:8000/api/tasks/ `
  -H "Authorization: Token TU_TOKEN" `
  -H "Content-Type: application/json" `
  -d '{"title":"Revisar la API"}'

Usa esas credenciales solo en local. Los tokens deben viajar por HTTPS, quedar fuera de logs y repositorios y poder revocarse. La autenticación por token integrada es deliberadamente sencilla. Caducidad, rotación, clientes delegados o scopes pueden requerir OAuth2 u otro diseño. La documentación de autenticación de DRF explica las alternativas.

Pruebas automatizadas

Las pruebas deben demostrar autenticación, validación y aislamiento. Añade tasks/tests.py:

from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase

from .models import Task


class TaskApiTests(APITestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="ana", password="clave-local-segura"
        )
        self.client.force_authenticate(self.user)

    def test_create_task_assigns_owner(self):
        response = self.client.post("/api/tasks/", {"title": "Probar endpoint"})
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertTrue(Task.objects.filter(owner=self.user).exists())

    def test_rejects_short_title(self):
        response = self.client.post("/api/tasks/", {"title": "x"})
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

Ejecuta python manage.py test. Agrega casos sin credenciales, con el ID de otra cuenta, paginación y métodos prohibidos. La guía de pruebas con pytest sirve si el proyecto adopta pytest.

Errores, rendimiento y producción

DRF transforma ValidationError en una respuesta 400 estructurada. No captures toda Exception para devolver siempre 200 ni expongas tracebacks. Utiliza 401 para autenticación ausente o inválida, 403 cuando una identidad autenticada carece de permiso, 404 para recursos no disponibles y 400 para entrada inválida. Registra un identificador de correlación y contexto interno, nunca contraseñas, tokens o payloads sensibles completos. La guía de logging en Python profundiza en el contexto diagnóstico.

Evita consultas N+1 con select_related() y prefetch_related() cuando el serializer recorra relaciones. La paginación limita cada respuesta, pero también hacen falta throttling, límites de cuerpo en el proxy y timeouts. Versiona el contrato antes de cambios incompatibles para consumidores externos. Restringe CORS a los orígenes necesarios: CORS es una regla del navegador, no autenticación.

Despliega con DEBUG=False, secreto obtenido del entorno, ALLOWED_HOSTS reducido, HTTPS, cookies seguras cuando correspondan y un servidor de aplicaciones de producción. Ejecuta las comprobaciones de despliegue en CI. La lista de despliegue de Django documenta los ajustes relevantes.

Fuentes oficiales

Preguntas frecuentes

¿Un serializer es igual que un formulario Django?

No. Ambos validan valores, pero los serializers representan recursos API, negocian formatos y crean o actualizan objetos desde payloads.

¿Debo elegir ViewSet o APIView?

Usa ModelViewSet para CRUD convencional. Elige APIView cuando el endpoint sea una operación que no encaje naturalmente en la semántica CRUD.

¿TokenAuthentication basta para producción?

Puede bastar en un sistema sencillo con HTTPS, almacenamiento seguro y revocación. Caducidad, rotación, autorización delegada y scopes requieren un mecanismo más completo.

¿Cómo impido que los usuarios vean datos ajenos?

Filtra cada queryset por la identidad autenticada y prueba lista y detalle. Añade permisos por objeto para reglas más ricas, sin eliminar el aislamiento básico de la consulta.

Conclusión

Una API DRF fiable establece límites explícitos: serializers controlan entrada y salida, querysets imponen propiedad, autenticación identifica y permisos autorizan. El CRUD compacto es solo el comienzo. Valores cerrados, validación, paginación, pruebas de aislamiento, errores coherentes y configuración de producción permiten crecer sin convertir comodidad en exposición de datos.