O Django REST Framework (DRF) é o principal conjunto de ferramentas para criar Web APIs com Django. Disponibiliza serialização, autenticação, viewsets e documentação navegável da API pronta a usar. Este guia aborda a criação de APIs REST prontas para produção, na perspetiva de um programador sénior.
Porquê o Django REST Framework
O DRF oferece vantagens convincentes:
- Desenvolvimento Rápido: Viewsets e routers minimizam o código repetitivo
- Serialização: Tipos de dados complexos para Python nativo e de volta
- Autenticação: Suporte para token, sessão, JWT e OAuth
- API Navegável: Documentação interativa sem custos adicionais
- Throttling: Limitação de taxa integrada
Configuração e Instalação
Criar Projeto Django
# Criar ambiente virtual
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Instalar Django e DRF
pip install django djangorestframework
pip install django-filter # Para filtragem
# Criar projeto e app
django-admin startproject myproject
cd myproject
python manage.py startapp api
Configurar Settings
myproject/settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Terceiros
'rest_framework',
'django_filters',
# Local
'api',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
Models
api/models.py:
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
Executar migrações:
python manage.py makemigrations
python manage.py migrate
Serializers
api/serializers.py:
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Post, Category, Comment
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name']
read_only_fields = ['id']
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'post_count']
def get_post_count(self, obj):
return obj.posts.count()
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'author', 'content', 'created_at']
read_only_fields = ['id', 'created_at']
class PostListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for list views"""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'author', 'category',
'status', 'created_at', 'comment_count']
def get_comment_count(self, obj):
return obj.comments.count()
class PostDetailSerializer(serializers.ModelSerializer):
"""Full serializer with nested comments"""
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True,
required=False
)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'author',
'category', 'category_id', 'status',
'created_at', 'updated_at', 'comments']
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters")
return value
ViewSets e Views
api/views.py:
from rest_framework import viewsets, permissions, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Category, Comment
from .serializers import (
PostListSerializer, PostDetailSerializer,
CategorySerializer, CommentSerializer
)
class IsAuthorOrReadOnly(permissions.BasePermission):
"""Custom permission: only author can edit/delete"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author', 'category').prefetch_related('comments')
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'category', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'title']
lookup_field = 'slug'
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
return PostDetailSerializer
def get_queryset(self):
queryset = super().get_queryset()
# Utilizadores não autenticados veem apenas posts publicados
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
# Utilizadores autenticados veem os seus próprios posts e os publicados
elif not self.request.user.is_staff:
queryset = queryset.filter(
models.Q(status='published') | models.Q(author=self.request.user)
)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
post = self.get_object()
if post.author != request.user and not request.user.is_staff:
return Response(
{'error': 'Only author can publish'},
status=status.HTTP_403_FORBIDDEN
)
post.status = 'published'
post.save()
return Response({'status': 'published'})
@action(detail=True, methods=['get', 'post'])
def comments(self, request, slug=None):
post = self.get_object()
if request.method == 'GET':
comments = post.comments.all()
serializer = CommentSerializer(comments, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user, post=post)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
lookup_field = 'slug'
Encaminhamento de URLs
api/urls.py:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'posts', views.PostViewSet)
router.register(r'categories', views.CategoryViewSet)
urlpatterns = [
path('', include(router.urls)),
]
myproject/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('api-auth/', include('rest_framework.urls')), # Login da API navegável
]
Autenticação
Autenticação por Token
pip install djangorestframework-simplejwt
myproject/settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}
api/urls.py:
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
# ... rotas existentes
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Utilização:
# Obter tokens
curl -X POST http://localhost:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
# Usar token de acesso
curl http://localhost:8000/api/posts/ \
-H "Authorization: Bearer <access_token>"
Filtragem e Pesquisa
api/filters.py:
import django_filters
from .models import Post
class PostFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
created_after = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='gte'
)
created_before = django_filters.DateTimeFilter(
field_name='created_at',
lookup_expr='lte'
)
class Meta:
model = Post
fields = ['status', 'category', 'author', 'title',
'created_after', 'created_before']
Utilizar no viewset:
from .filters import PostFilter
class PostViewSet(viewsets.ModelViewSet):
filterset_class = PostFilter
# ...
Testes
api/tests.py:
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from .models import Post, Category
class PostAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='Tech',
slug='tech'
)
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user,
category=self.category,
status='published'
)
self.client = APIClient()
def test_list_posts(self):
response = self.client.get('/api/posts/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_create_post_authenticated(self):
self.client.force_authenticate(user=self.user)
data = {
'title': 'New Post',
'slug': 'new-post',
'content': 'New content',
'category_id': self.category.id,
}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Post.objects.count(), 2)
def test_create_post_unauthenticated(self):
data = {'title': 'New Post', 'content': 'Content'}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_update_own_post(self):
self.client.force_authenticate(user=self.user)
response = self.client.patch(
f'/api/posts/{self.post.slug}/',
{'title': 'Updated Title'}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Title')
def test_cannot_update_others_post(self):
other_user = User.objects.create_user('other', password='pass')
self.client.force_authenticate(user=other_user)
response = self.client.patch(
f'/api/posts/{self.post.slug}/',
{'title': 'Hacked'}
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Executar testes:
python manage.py test api
Principais Conclusões
- Use ViewSets: Minimize o código repetitivo para operações CRUD
- Permissões personalizadas: Implemente a lógica de negócio em classes de permissões
- Otimize queries: Use
selectrelated e prefetchrelated - Serializers diferentes: Vistas de lista vs. detalhe precisam de dados diferentes
- Backends de filtros: Ative consultas avançadas sem código personalizado
- Teste exaustivamente: Testes de API detetam problemas de integração cedo
O Django REST Framework fornece tudo o que é necessário para APIs robustas e seguras, mantendo a produtividade pela qual o Django é conhecido.