Django REST Framework: Professional API za 20 minút
11 min
Django REST Framework: Professional API za 20 minút
"Potrebujem production-ready REST API s dokumentáciou!" 🔥
Django REST Framework (DRF) je odpoveď.
Prečo Django REST Framework?
DRF je professional choice pre REST APIs v Pythone.
DRF features:
- 🔥 Industry standard (Instagram, Mozilla, Red Hat...)
- 📚 Auto-generated browsable API
- 🔐 Built-in authentication (Token/Session/JWT)
- ✅ Automatic validation with serializers
- 🎨 Test endpoints directly in browser!
- 🚀 Production-ready
Poďme na to!
Quick Start: 20 minút od nuly po API
Krok 1: Installation (2 minúty)
bash
# Virtual environment
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Install Django + DRF
pip install django djangorestframework
# Verify
python -c "import rest_framework; print(rest_framework.__version__)"
# Output: 3.14.0Krok 2: Project Setup (2 minúty)
bash
# Create Django project
django-admin startproject myapi
cd myapi
# Create app for API
python manage.py startapp products
# Štruktúra:
# myapi/
# manage.py
# myapi/
# settings.py
# urls.py
# products/
# models.py
# views.py
# serializers.py <- Will create this
# urls.py <- Will create thisKrok 3: Configuration (2 minúty)
python
# myapi/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework', # <- Add DRF
# Your apps
'products', # <- Your app
]
# DRF Configuration
REST_FRAMEWORK = {
# Pagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
# Default permissions (we'll customize later)
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny', # For now
],
}Krok 4: Models (3 minúty)
python
# products/models.py
from django.db import models
class Category(models.Model):
"""Product category"""
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = 'categories'
ordering = ['name']
def __str__(self):
return self.name
class Product(models.Model):
"""Product model"""
# Basic info
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
# Pricing
price = models.DecimalField(max_digits=10, decimal_places=2)
# Inventory
stock = models.IntegerField(default=0)
# Category
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='products'
)
# Status
active = models.BooleanField(default=True)
# Timestamps
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.nameKrok 5: Serializers (3 minúty)
python
# products/serializers.py (create this file)
from rest_framework import serializers
from .models import Category, Product
class CategorySerializer(serializers.ModelSerializer):
"""Serialize Category model"""
product_count = serializers.IntegerField(read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'product_count']
read_only_fields = ['id']
class ProductSerializer(serializers.ModelSerializer):
"""Serialize Product model"""
# Nested category (read)
category_detail = CategorySerializer(source='category', read_only=True)
# Simple category ID (write)
category_id = serializers.IntegerField(write_only=True)
class Meta:
model = Product
fields = [
'id',
'name',
'slug',
'description',
'price',
'stock',
'category_id', # For writing
'category_detail', # For reading
'active',
'created_at',
'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
def validate_price(self, value):
"""Custom validation for price"""
if value < 0:
raise serializers.ValidationError("Price cannot be negative")
return value
def validate_stock(self, value):
"""Custom validation for stock"""
if value < 0:
raise serializers.ValidationError("Stock cannot be negative")
return valueSerializers handle:
- ✅ Model → JSON (serialization)
- ✅ JSON → Model (deserialization)
- ✅ Validation
- ✅ Nested relationships
Krok 6: Views (3 minúty)
python
# products/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Count
from .models import Category, Product
from .serializers import CategorySerializer, ProductSerializer
class CategoryViewSet(viewsets.ModelViewSet):
"""
ViewSet for Category model
Provides:
- list: GET /api/categories/
- create: POST /api/categories/
- retrieve: GET /api/categories/1/
- update: PUT /api/categories/1/
- partial_update: PATCH /api/categories/1/
- destroy: DELETE /api/categories/1/
"""
queryset = Category.objects.annotate(
product_count=Count('products')
)
serializer_class = CategorySerializer
class ProductViewSet(viewsets.ModelViewSet):
"""
ViewSet for Product model
All CRUD operations + custom actions
"""
queryset = Product.objects.select_related('category').all()
serializer_class = ProductSerializer
def get_queryset(self):
"""
Filter queryset by query parameters
Examples:
?active=true
?category=1
?search=laptop
"""
queryset = super().get_queryset()
# Filter by active
active = self.request.query_params.get('active')
if active is not None:
queryset = queryset.filter(active=active.lower() == 'true')
# Filter by category
category = self.request.query_params.get('category')
if category:
queryset = queryset.filter(category_id=category)
# Simple search
search = self.request.query_params.get('search')
if search:
queryset = queryset.filter(name__icontains=search)
return queryset
@action(detail=False, methods=['get'])
def low_stock(self, request):
"""
Custom action: Get products with low stock
GET /api/products/low_stock/
"""
threshold = int(request.query_params.get('threshold', 10))
products = self.get_queryset().filter(stock__lt=threshold)
serializer = self.get_serializer(products, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def restock(self, request, pk=None):
"""
Custom action: Add stock to product
POST /api/products/1/restock/
Body: {"amount": 50}
"""
product = self.get_object()
amount = request.data.get('amount', 0)
try:
amount = int(amount)
if amount < 0:
return Response(
{'error': 'Amount must be positive'},
status=status.HTTP_400_BAD_REQUEST
)
product.stock += amount
product.save()
return Response({
'status': 'restocked',
'product': product.name,
'new_stock': product.stock
})
except ValueError:
return Response(
{'error': 'Invalid amount'},
status=status.HTTP_400_BAD_REQUEST
)ViewSets provide:
- ✅ All CRUD operations automatically
- ✅ Custom actions (@action decorator)
- ✅ Filtering
- ✅ Permissions (add later)
Krok 7: URLs (2 minúty)
python
# products/urls.py (create this file)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet, ProductViewSet
# Router automatically generates URL patterns
router = DefaultRouter()
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'products', ProductViewSet, basename='product')
# Generated URLs:
# /categories/ GET, POST
# /categories/1/ GET, PUT, PATCH, DELETE
# /products/ GET, POST
# /products/1/ GET, PUT, PATCH, DELETE
# /products/low_stock/ GET (custom action)
# /products/1/restock/ POST (custom action)
urlpatterns = [
path('', include(router.urls)),
]python
# myapi/urls.py (update this)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('products.urls')),
# DRF browsable API login
path('api-auth/', include('rest_framework.urls')),
]Krok 8: Migrate and Run (2 minúty)
bash
# Create migrations
python manage.py makemigrations
# Apply migrations
python manage.py migrate
# Create superuser (optional, for admin)
python manage.py createsuperuser
# Username: admin
# Password: ********
# Run server
python manage.py runserver
# Open browser:
# http://127.0.0.1:8000/api/
#
# 🎉 BROWSABLE API! 🎉
# You can see AND test endpoints in browser!You'll see:
API Root
────────
{
"categories": "http://localhost:8000/api/categories/",
"products": "http://localhost:8000/api/products/"
}Click around! You can test API directly in browser! 🎨
Krok 9: Test API (2 minúty)
bash
# CREATE CATEGORY
curl -X POST http://localhost:8000/api/categories/ \
-H "Content-Type: application/json" \
-d '{
"name":"Electronics",
"slug":"electronics",
"description":"Electronic devices"
}'
# Response: {"id":1,"name":"Electronics",...}
# CREATE PRODUCT
curl -X POST http://localhost:8000/api/products/ \
-H "Content-Type: application/json" \
-d '{
"name":"Laptop",
"slug":"laptop",
"description":"Gaming laptop",
"price":"999.99",
"stock":5,
"category_id":1
}'
# Response: {"id":1,"name":"Laptop",...}
# LIST PRODUCTS
curl http://localhost:8000/api/products/
# Response: {"count":1,"next":null,"previous":null,"results":[...]}
# GET ONE PRODUCT
curl http://localhost:8000/api/products/1/
# Response: {"id":1,"name":"Laptop",...}
# UPDATE PRODUCT
curl -X PATCH http://localhost:8000/api/products/1/ \
-H "Content-Type: application/json" \
-d '{"price":"899.99"}'
# Response: {"id":1,"price":"899.99",...}
# FILTER PRODUCTS
curl http://localhost:8000/api/products/?active=true
curl http://localhost:8000/api/products/?category=1
curl http://localhost:8000/api/products/?search=laptop
# CUSTOM ACTION: Low stock
curl http://localhost:8000/api/products/low_stock/?threshold=10
# CUSTOM ACTION: Restock
curl -X POST http://localhost:8000/api/products/1/restock/ \
-H "Content-Type: application/json" \
-d '{"amount":50}'
# Response: {"status":"restocked","new_stock":55}
# DELETE PRODUCT
curl -X DELETE http://localhost:8000/api/products/1/
# Response: 204 No Content
# All working! 🎉Total time: 20 minút!
Advanced Features
Authentication & Permissions
python
# myapi/settings.py - Update REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
# Add to INSTALLED_APPS
INSTALLED_APPS += [
'rest_framework.authtoken',
]bash
# Migrate to create token tables
python manage.py migrate
# Create token for user
python manage.py drf_create_token admin
# Generated token for user admin: abc123def456...bash
# Now API requires authentication for write operations
# READ (works without auth)
curl http://localhost:8000/api/products/
# WRITE (needs auth)
curl -X POST http://localhost:8000/api/products/ \
-H "Authorization: Token abc123def456..." \
-H "Content-Type: application/json" \
-d '{"name":"Mouse","price":"29.99",...}'
# Token auth working! 🔐Custom Permissions
python
# products/permissions.py (create this)
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission:
- Read: Anyone
- Write: Only authenticated users
- Delete: Only superusers
"""
def has_permission(self, request, view):
# Read permissions (GET, HEAD, OPTIONS)
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions: authenticated users
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Read: anyone
if request.method in permissions.SAFE_METHODS:
return True
# Update: authenticated users
if request.method in ['PUT', 'PATCH']:
return request.user and request.user.is_authenticated
# Delete: only superusers
if request.method == 'DELETE':
return request.user and request.user.is_superuser
# products/views.py - Use custom permission
from .permissions import IsOwnerOrReadOnly
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsOwnerOrReadOnly] # Apply permissionFiltering & Search
bash
# Install django-filter
pip install django-filterpython
# myapi/settings.py
INSTALLED_APPS += ['django_filters']
REST_FRAMEWORK = {
# ... existing config ...
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
# products/views.py - Add filtering
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
# Filtering
filterset_fields = ['category', 'active'] # Exact match
search_fields = ['name', 'description'] # Search
ordering_fields = ['price', 'created_at', 'stock'] # Ordering
ordering = ['-created_at'] # Default orderingNow you can:
bash
# Filter by exact match
curl http://localhost:8000/api/products/?category=1
curl http://localhost:8000/api/products/?active=true
# Search in name/description
curl http://localhost:8000/api/products/?search=laptop
# Order by price
curl http://localhost:8000/api/products/?ordering=price
curl http://localhost:8000/api/products/?ordering=-price # Descending
# Combine!
curl "http://localhost:8000/api/products/?category=1&search=gaming&ordering=-price"
# Powerful filtering! 🎯Statistics Endpoint
python
# products/views.py
from django.db.models import Count, Avg, Sum, Min, Max
class ProductViewSet(viewsets.ModelViewSet):
# ... existing code ...
@action(detail=False, methods=['get'])
def statistics(self, request):
"""
GET /api/products/statistics/
Returns various statistics about products
"""
queryset = self.get_queryset()
stats = {
'total_products': queryset.count(),
'active_products': queryset.filter(active=True).count(),
'total_stock': queryset.aggregate(Sum('stock'))['stock__sum'] or 0,
'avg_price': float(queryset.aggregate(Avg('price'))['price__avg'] or 0),
'min_price': float(queryset.aggregate(Min('price'))['price__min'] or 0),
'max_price': float(queryset.aggregate(Max('price'))['price__max'] or 0),
'by_category': list(
queryset.values('category__name')
.annotate(count=Count('id'))
.order_by('-count')
)
}
return Response(stats)
# Test:
# curl http://localhost:8000/api/products/statistics/Throttling (Rate Limiting)
python
# myapi/settings.py
REST_FRAMEWORK = {
# ... existing config ...
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', # Anonymous users
'user': '1000/day', # Authenticated users
}
}
# Now API is rate-limited! ⚡
# Anonymous: 100 requests/day
# Authenticated: 1000 requests/dayVersioning
python
# myapi/settings.py
REST_FRAMEWORK = {
# ... existing config ...
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# myapi/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('products.urls')), # Version 1
path('api-auth/', include('rest_framework.urls')),
]
# Now API is versioned!
# http://localhost:8000/api/v1/products/Testing
python
# products/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth.models import User
from .models import Category, Product
class ProductAPITestCase(APITestCase):
def setUp(self):
"""Setup test data"""
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='Test Category',
slug='test-category'
)
self.product = Product.objects.create(
name='Test Product',
slug='test-product',
price=99.99,
stock=10,
category=self.category
)
def test_list_products(self):
"""Test listing products"""
response = self.client.get('/api/products/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
def test_get_product(self):
"""Test getting single product"""
response = self.client.get(f'/api/products/{self.product.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], 'Test Product')
def test_create_product_authenticated(self):
"""Test creating product (authenticated)"""
self.client.force_authenticate(user=self.user)
data = {
'name': 'New Product',
'slug': 'new-product',
'price': '49.99',
'stock': 5,
'category_id': self.category.id
}
response = self.client.post('/api/products/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Product.objects.count(), 2)
def test_create_product_unauthenticated(self):
"""Test creating product (unauthenticated) - should fail"""
data = {
'name': 'New Product',
'slug': 'new-product',
'price': '49.99',
'stock': 5,
'category_id': self.category.id
}
response = self.client.post('/api/products/', data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_update_product(self):
"""Test updating product"""
self.client.force_authenticate(user=self.user)
data = {'price': '79.99'}
response = self.client.patch(
f'/api/products/{self.product.id}/',
data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.product.refresh_from_db()
self.assertEqual(float(self.product.price), 79.99)
def test_filter_by_category(self):
"""Test filtering by category"""
response = self.client.get(
f'/api/products/?category={self.category.id}'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
def test_search_products(self):
"""Test searching products"""
response = self.client.get('/api/products/?search=Test')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 1)
# Run tests:
# python manage.py test productsProduction Deployment
python
# myapi/settings.py - Production settings
import os
# Security
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY')
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# HTTPS
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Database (example with PostgreSQL)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# CORS (if needed for frontend)
# pip install django-cors-headers
INSTALLED_APPS += ['corsheaders']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
CORS_ALLOWED_ORIGINS = [
'https://yourdomain.com',
'https://www.yourdomain.com',
]bash
# Install production requirements
pip install gunicorn psycopg2-binary django-cors-headers
# Collect static files
python manage.py collectstatic
# Run with gunicorn
gunicorn myapi.wsgi:application --bind 0.0.0.0:8000
# Production ready! 🚀Záver
DRF features v 20 minútach:
- ✅ Browsable API (test in browser!)
- ✅ Auto validation
- ✅ Authentication ready
- ✅ Permissions system
- ✅ Filtering & search
- ✅ Pagination
- ✅ Throttling
- ✅ Versioning
- ✅ Testing tools
Setup za 20 minút. Production ready. Industry standard.
DRF používajú: Instagram, Mozilla, Red Hat, Eventbrite...
Tutorial napísal developer ktorý používa DRF v produkcii. Sometimes paying 20 minút setup saves weeks of manual work. DRF je proof. 🔥