Lánzate a Python Django REST Framework: tu primer paso concreto hoy
Python Django REST Framework : lo esencial en un artículo — código real, diagramas y pasos concretos, extractos de un curso de 24 lecciones.
La mejor forma de aprender Python Django REST Framework es practicando. Este artículo te ayuda a empezar con extractos prácticos extraídos de un curso de 24 lecciones — para obtener un primer resultado ya hoy.
tl;dr
- Setup DRF
- Serializers
- Views
- Auth y permisos
- Filtros y paginación
~$ cat ./parcours.md # Python Django REST Framework — 8 capítulos
01
Setup DRF
→ REST vs RPC vs GraphQL→ Instalación y primer endpoint+ 1 más lecciones
02
Serializers
→ Serializer y ModelSerializer→ Validación custom+ 1 más lecciones
03
Views
→ APIView y generics→ ViewSets y Routers+ 1 más lecciones
04
Auth y permisos
→ Token, Session, JWT→ Permisos builtin y custom+ 1 más lecciones
05
Filtros y paginación
→ Filters y search→ Pagination+ 1 más lecciones
06
Documentación OpenAPI
→ drf-spectacular→ Swagger UI y Redoc+ 1 más lecciones
07
Tests y performance
→ APITestCase y factories→ Optimización N+1+ 1 más lecciones
08
Proyecto final API SaaS
→ Arquitectura multi-tenant→ Endpoints y tests+ 1 más lecciones
🏁
Proyecto final
→ Te vas con un proyecto concreto y demostrable
REST vs RPC vs GraphQL
REST : recursos y verbos
GET /api/articles/ # listar GET /api/articles/42/ # detalle POST /api/articles/ # crear PUT /api/articles/42/ # reemplazar PATCH /api/articles/42/ # actualización parcial DELETE /api/articles/42/ # eliminar # Verbos HTTP + URIs = CRUD universal
Comparación
| Aspecto | REST | GraphQL | RPC (gRPC) |
|---|---|---|---|
| Formato | JSON/XML | JSON | Protobuf |
| Transporte | HTTP | HTTP | HTTP/2 |
| Esquema | OpenAPI | SDL (tipado) | .proto |
| Versionado | v1/v2 | campos obsoletos | compatibilidad hacia atrás |
| Over-fetch | Sí | No | No |
| Caché HTTP | Fácil | Difícil | No |
| Herramientas | Excelente | Bueno | Específico de la tecnología |
| Móvil/IoT | OK | Excelente | Excelente |
Cuándo elegir qué
NOTEREST — APIs públicas, CRUD simple, web/móvil clásico, cacheable. Opción por defecto en el 90 % de los casos.
NOTEGraphQL — Móvil complejo con pantallas que agregan 5+ recursos, frontends múltiples con necesidades distintas.
NOTEgRPC — Microservicios internos, latencia crítica, streaming bidireccional, múltiples lenguajes.
¿Por qué DRF?
Alternativas a DRF
Anatomía de una buena API REST
# Buenas prácticas GET /api/v1/users/ # lista GET /api/v1/users/42/ # detalle GET /api/v1/users/42/orders/ # subrecurso POST /api/v1/users/ # 201 Created + Location PATCH /api/v1/users/42/ # parcial DELETE /api/v1/users/42/ # 204 No Content # Filtros / paginación GET /api/v1/users/?role=admin&page=2&page_size=20 # Códigos de estado 200 OK, 201 Created, 204 No Content 400 Bad Request, 401 Unauthorized, 403 Forbidden 404 Not Found, 409 Conflict, 422 Unprocessable 500 Server Error, 503 Unavailable
Formato de respuesta estándar
{
"data": [...],
"meta": {
"count": 150,
"page": 2,
"page_size": 20
},
"links": {
"next": "/api/v1/users/?page=3",
"prev": "/api/v1/users/?page=1"
}
}
# Errores
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": {"email": ["This field is required"]}
}
}Resumen
NOTEPara recordar
- REST = recursos + verbos HTTP estandarizados
- GraphQL para móvil complejo, REST para el resto
- DRF = estándar de Django desde 2014
- v1/v2 en la URL, formato JSON, códigos HTTP correctos
Arquitectura multi-tenant
Requisitos
NOTEProyecto — API SaaS de gestión de proyectos/tareas multi-tenant con planes de precios (Free/Pro/Enterprise), facturación con Stripe, webhooks y OpenAPI público.
Modelos
# workspaces/models.py class Workspace(models.Model): class Plan(models.TextChoices): FREE = "free", "Free" PRO = "pro", "Pro" ENTERPRISE = "enterprise", "Enterprise" name = models.CharField(max_length=100) slug = models.SlugField(unique=True) plan = models.CharField(max_length=20, choices=Plan.choices, default=Plan.FREE) stripe_customer_id = models.CharField(max_length=100, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Membership(models.Model): class Role(models.TextChoices): OWNER = "owner" ADMIN = "admin" MEMBER = "member" GUEST = "guest" workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name="memberships") user = models.ForeignKey("accounts.User", on_delete=models.CASCADE) role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER) invited_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ["workspace", "user"] # projects/models.py class Project(models.Model): workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name="projects") name = models.CharField(max_length=200) description = models.TextField(blank=True) created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [models.Index(fields=["workspace", "-created_at"])] class Task(models.Model): class Status(models.TextChoices): TODO = "todo" IN_PROGRESS = "in_progress" DONE = "done" project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="tasks") title = models.CharField(max_length=200) description = models.TextField(blank=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.TODO) assigned_to = models.ForeignKey("accounts.User", null=True, blank=True, on_delete=models.SET_NULL) due_date = models.DateField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
Tenant mediante subdominio
# workspaces/middleware.py class WorkspaceMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): host = request.get_host().split(":")[0] subdomain = host.split(".")[0] try: request.workspace = Workspace.objects.get(slug=subdomain) except Workspace.DoesNotExist: request.workspace = None return self.get_response(request) # settings.py MIDDLEWARE += ["workspaces.middleware.WorkspaceMiddleware"] # URLs : acme.api.example.com -> workspace "acme"
Permiso multi-tenant
# workspaces/permissions.py from rest_framework.permissions import BasePermission class IsWorkspaceMember(BasePermission): def has_permission(self, request, view): if not request.user.is_authenticated: return False if not request.workspace: return False return Membership.objects.filter( workspace=request.workspace, user=request.user ).exists() class IsWorkspaceAdmin(BasePermission): def has_permission(self, request, view): return Membership.objects.filter( workspace=request.workspace, user=request.user, role__in=["owner", "admin"] ).exists()
Mixin para queryset con filtro automático
class WorkspaceQuerysetMixin: """Filtro automático por workspace actual.""" workspace_field = "workspace" def get_queryset(self): qs = super().get_queryset() if self.request.workspace: return qs.filter(**{self.workspace_field: self.request.workspace}) return qs.none() def perform_create(self, serializer): serializer.save(workspace=self.request.workspace, created_by=self.request.user) class ProjectViewSet(WorkspaceQuerysetMixin, viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer permission_classes = [IsAuthenticated, IsWorkspaceMember] def get_queryset(self): return super().get_queryset().select_related("created_by") class TaskViewSet(WorkspaceQuerysetMixin, viewsets.ModelViewSet): queryset = Task.objects.all() serializer_class = TaskSerializer permission_classes = [IsAuthenticated, IsWorkspaceMember] workspace_field = "project__workspace" def get_queryset(self): return super().get_queryset().select_related("project", "assigned_to")
Cuotas por plan
# workspaces/quotas.py PLAN_LIMITS = { "free": {"max_projects": 3, "max_members": 3, "api_rate": "100/day"}, "pro": {"max_projects": None, "max_members": 20, "api_rate": "10000/day"}, "enterprise": {"max_projects": None, "max_members": None, "api_rate": None}, } def check_project_quota(workspace): limit = PLAN_LIMITS[workspace.plan]["max_projects"] if limit is None: return current = workspace.projects.count() if current >= limit: raise serializers.ValidationError( f"Límite alcanzado: {limit} proyectos para el plan {workspace.plan}. " f"Actualiza a Pro." ) # En el serializer class ProjectSerializer(serializers.ModelSerializer): def create(self, validated_data): check_project_quota(validated_data["workspace"]) return super().create(validated_data)
Throttle por plan
class PlanRateThrottle(SimpleRateThrottle): scope = "plan" def allow_request(self, request, view): if not request.workspace: return True rate = PLAN_LIMITS[request.workspace.plan]["api_rate"] if rate is None: return True # enterprise self.num_requests, self.duration = self.parse_rate(rate) self.request = request return super().allow_request(request, view) def get_cache_key(self, request, view): return f"plan_{request.workspace.id}"
Almacenamiento en caché de respuestas
Configurar la caché
# settings.py - backend Redis CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": env("REDIS_URL"), "TIMEOUT": 300, # 5 min por defecto "OPTIONS": { "db": 1, "max_connections": 50, } } } # Prueba rápida con memoria local (desarrollo) CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"} }
Caché de vista completa
from django.views.decorators.cache import cache_page from django.utils.decorators import method_decorator class PopularBooksView(generics.ListAPIView): queryset = Book.objects.filter(popular=True) serializer_class = BookSerializer @method_decorator(cache_page(60 * 15)) # 15 min def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # O con vary_on_headers from django.views.decorators.vary import vary_on_headers @method_decorator(cache_page(600), name="dispatch") @method_decorator(vary_on_headers("Authorization"), name="dispatch") class MyView(generics.ListAPIView): # Caché diferente por token ...
Caché de bajo nivel
from django.core.cache import cache class StatsView(APIView): def get(self, request): cache_key = f"stats_{request.user.id}" data = cache.get(cache_key) if data is None: # Cálculo costoso data = compute_expensive_stats(request.user) cache.set(cache_key, data, timeout=3600) return Response(data) # get_or_set en 1 línea data = cache.get_or_set(f"stats_{user.id}", lambda: compute_stats(user), 3600) # Multi-caché cache.set_many({"a": 1, "b": 2}, timeout=60) values = cache.get_many(["a", "b"]) # Invalidación cache.delete("stats_42") cache.delete_many(["stats_42", "stats_43"]) cache.clear() # borrar todo
Invalidación de caché mediante signals
from django.db.models.signals import post_save, post_delete from django.dispatch import receiver @receiver([post_save, post_delete], sender=Book) def invalidate_books_cache(sender, instance, **kwargs): cache.delete("popular_books") cache.delete(f"book_{instance.id}") cache.delete(f"books_by_author_{instance.author_id}")
Caché HTTP : ETag
from rest_framework.response import Response import hashlib class BookDetailView(generics.RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer def retrieve(self, request, *args, **kwargs): book = self.get_object() etag = hashlib.md5(str(book.updated_at).encode()).hexdigest() if request.headers.get("If-None-Match") == etag: return Response(status=304) # Not Modified response = Response(BookSerializer(book).data) response["ETag"] = etag response["Cache-Control"] = "public, max-age=300" return response # El cliente envía automáticamente If-None-Match: "abc..." # El servidor puede responder 304 sin body -> muy rápido
Cache-Control por endpoint
from django.views.decorators.cache import cache_control from django.utils.decorators import method_decorator class PublicCatalogView(generics.ListAPIView): queryset = Product.objects.filter(public=True) serializer_class = ProductSerializer @method_decorator(cache_control(public=True, max_age=3600)) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # Header : Cache-Control: public, max-age=3600 # CDN y navegador lo cachearán class UserDataView(APIView): @method_decorator(cache_control(private=True, max_age=60)) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # private = sin caché CDN, pero sí en el navegador
va-plus-loin
Este artículo cubre los extractos más útiles — el curso completo Python Django REST Framework (8 capítulos, 24 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.
./acceder-al-curso-completo curso gratuito : Vibe CodingFAQ
¿Cuánto tiempo se necesita para aprender Python Django REST Framework?
Con una progresión estructurada (8 capítulos, 24 lecciones cortas y prácticas), se alcanza un nivel operativo en unas semanas dedicando 30 a 60 minutos al día. Lo importante es practicar cada concepto de inmediato.
¿Se necesitan requisitos previos?
Basta con nociones básicas de informática. Si sabes usar una terminal y leer código sencillo, estás listo.
¿Por dónde empezar de forma concreta?
Reproduce los comandos de este artículo y luego sigue el curso completo Python Django REST Framework: encadena las 24 lecciones en orden, con ejercicios y proyecto final.
./a-lire-aussi
→ Lánzate con Portfolio IA SEO Vercel : tu primer paso concreto hoy→ IA Stripe GitHub SaaS en la práctica : el código y los comandos que realmente importan→ Python Requests APIs explicado de forma sencilla (con diagramas y código real)📬 ¿Quieres recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero palabrería.