Comece com Python Django REST Framework: seu primeiro passo concreto hoje
Python Django REST Framework: o essencial em um artigo — código real, diagramas e etapas concretas, extratos de um curso de 24 lições.
A melhor forma de aprender Python Django REST Framework é fazendo. Este artigo te dá o pontapé inicial com trechos práticos extraídos de um curso de 24 lições — o suficiente para obter um primeiro resultado já hoje.
tl;dr
- Setup DRF
- Serializers
- Views
- Autenticação e permissões
- Filtros e paginação
~$ cat ./parcours.md # Python Django REST Framework — 8 capítulos
01
Setup DRF
→ REST vs RPC vs GraphQL→ Instalação e primeiro endpoint+ 1 mais lições
02
Serializers
→ Serializer e ModelSerializer→ Validação custom+ 1 mais lições
03
Views
→ APIView e generics→ ViewSets e Routers+ 1 mais lições
04
Auth e permissões
→ Token, Session, JWT→ Permissions builtin e custom+ 1 mais lições
05
Filtros e paginação
→ Filters e search→ Pagination+ 1 mais lições
06
Documentação OpenAPI
→ drf-spectacular→ Swagger UI e Redoc+ 1 mais lições
07
Testes e performance
→ APITestCase e factories→ Otimização N+1+ 1 mais lições
08
Projeto final API SaaS
→ Arquitetura multi-tenant→ Endpoints e tests+ 1 mais lições
🏁
Projeto final
→ Você sai com um projeto concreto e demonstrável
REST vs RPC vs GraphQL
REST : recursos e verbos
GET /api/articles/ # listar GET /api/articles/42/ # detalhe POST /api/articles/ # criar PUT /api/articles/42/ # substituir PATCH /api/articles/42/ # atualização parcial DELETE /api/articles/42/ # excluir # Verbos HTTP + URIs = CRUD universal
Comparação
| Aspecto | REST | GraphQL | RPC (gRPC) |
|---|---|---|---|
| Formato | JSON/XML | JSON | Protobuf |
| Transporte | HTTP | HTTP | HTTP/2 |
| Schema | OpenAPI | SDL (tipado) | .proto |
| Versionamento | v1/v2 | campos obsoletos | compatibilidade retroativa |
| Over-fetch | Sim | Não | Não |
| Cache HTTP | Fácil | Difícil | Não |
| Ferramentas | Excelente | Bom | Específico da tecnologia |
| Mobile/IoT | OK | Excelente | Excelente |
Quando escolher o quê
NOTEREST — APIs públicas, CRUD simples, web/mobile clássico, cacheável. Padrão para 90% dos casos.
NOTEGraphQL — Mobile complexo com tela agregando 5+ recursos, frontends múltiplos com necessidades diferentes.
NOTEgRPC — Microsserviços internos, latência crítica, streaming bidirecional, múltiplas linguagens.
Por que DRF?
Alternativas ao DRF
Anatomia de uma boa API REST
# Boas práticas GET /api/v1/users/ # lista GET /api/v1/users/42/ # detalhe GET /api/v1/users/42/orders/ # sub-recurso POST /api/v1/users/ # 201 Created + Location PATCH /api/v1/users/42/ # parcial DELETE /api/v1/users/42/ # 204 No Content # Filtros / paginação GET /api/v1/users/?role=admin&page=2&page_size=20 # Códigos de status 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 resposta padrão
{
"data": [...],
"meta": {
"count": 150,
"page": 2,
"page_size": 20
},
"links": {
"next": "/api/v1/users/?page=3",
"prev": "/api/v1/users/?page=1"
}
}
# Erros
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": {"email": ["This field is required"]}
}
}Resumo
NOTEPara lembrar
- REST = recursos + verbos HTTP padronizados
- GraphQL para mobile complexo, REST para o restante
- DRF = padrão Django desde 2014
- v1/v2 na URL, formato JSON, códigos HTTP corretos
Arquitetura multi-tenant
Especificações
NOTEProjeto — API SaaS de gerenciamento de projetos/tarefas multi-tenant com planos tarifários (Free/Pro/Enterprise), faturamento Stripe, webhooks e 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 via subdomínio
# 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"
Permissão 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 auto-filtrado
class WorkspaceQuerysetMixin: """Filtra automaticamente pelo workspace atual.""" 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")
Quotas por plano
# 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"Limite atingido: {limit} projetos para o plano {workspace.plan}. " f"Faça upgrade para Pro." ) # No serializer class ProjectSerializer(serializers.ModelSerializer): def create(self, validated_data): check_project_quota(validated_data["workspace"]) return super().create(validated_data)
Throttle por plano
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}"
Cache de respostas
Configurar o cache
# settings.py - Redis backend CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": env("REDIS_URL"), "TIMEOUT": 300, # 5 min padrão "OPTIONS": { "db": 1, "max_connections": 50, } } } # Teste rápido com local memory (dev) CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"} }
Cache de view 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) # OU com 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): # Cache diferente por token ...
Cache low-level
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 custoso data = compute_expensive_stats(request.user) cache.set(cache_key, data, timeout=3600) return Response(data) # get_or_set em 1 linha data = cache.get_or_set(f"stats_{user.id}", lambda: compute_stats(user), 3600) # Multi-cache cache.set_many({"a": 1, "b": 2}, timeout=60) values = cache.get_many(["a", "b"]) # Invalidação cache.delete("stats_42") cache.delete_many(["stats_42", "stats_43"]) cache.clear() # apagar tudo
Invalidação de cache via 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}")
HTTP cache : 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 # Cliente envia automaticamente If-None-Match: "abc..." # Servidor pode responder 304 sem body -> muito 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 e browser vão cachear class UserDataView(APIView): @method_decorator(cache_control(private=True, max_age=60)) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # private = sem cache CDN, mas browser sim
va-plus-loin
Este artigo cobre os trechos mais úteis — o curso completo Python Django REST Framework (8 capítulos, 24 lições, exercícios corrigidos e projeto final) te leva até o fim.
./acceder-au-cours-complet curso gratuito : Vibe CodingFAQ
Quanto tempo para aprender Python Django REST Framework?
Com uma progressão estruturada (8 capítulos, 24 lições curtas e práticas), você atinge um nível operacional em algumas semanas dedicando 30 a 60 minutos por dia. O importante é praticar cada conceito imediatamente.
Precisa de pré-requisitos?
Básicos de informática são suficientes. Se você sabe usar um terminal e ler código simples, está pronto.
Por onde começar na prática?
Reproduza os comandos deste artigo e depois siga o curso completo Python Django REST Framework: ele encadeia as 24 lições em ordem, com exercícios e projeto final.
./a-lire-aussi
→ Lance-toi en Portfolio IA SEO Vercel : ton premier pas concret aujourd'hui→ IA Stripe GitHub SaaS en pratique : le code et les commandes qui comptent vraiment→ Python Requests APIs expliqué simplement (avec schémas et vrai code)📬 Você quer receber este tipo de guia toda semana? Inscreva-se gratuitamente — código real, zero enrolação.