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.

Comece com Python Django REST Framework: seu primeiro passo concreto hoje

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

output
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

AspectoRESTGraphQLRPC (gRPC)
FormatoJSON/XMLJSONProtobuf
TransporteHTTPHTTPHTTP/2
SchemaOpenAPISDL (tipado).proto
Versionamentov1/v2campos obsoletoscompatibilidade retroativa
Over-fetchSimNãoNão
Cache HTTPFácilDifícilNão
FerramentasExcelenteBomEspecífico da tecnologia
Mobile/IoTOKExcelenteExcelente

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

output
# 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

output
{
  "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

output
# 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

output
# 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

output
# 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

output
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

output
# 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

output
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

output
# 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

output
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

output
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

output
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

output
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

output
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 Coding

FAQ

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.

📬 Você quer receber este tipo de guia toda semana? Inscreva-se gratuitamente — código real, zero enrolação.