Table of Contents
- Introduction
- Project Overview
- Prerequisites
- Setting Up the Environment
- Installing Dependencies
- Configuring Django Settings
- Understanding the Models
- Creating API Serializers
- Building API Views with Access Control
- Implementing JWT Authentication
- URL Configuration
- Testing the API
- Production Deployment
- Best Practices
- Troubleshooting
- Conclusion
Introduction
Building APIs is a crucial skill in modern web development. Whether you're creating a mobile app backend, integrating with frontend frameworks like React or Vue, or building microservices, a well-designed REST API is essential.
In this comprehensive tutorial, you'll learn how to build a production-ready Django REST API with JWT authentication and sophisticated access control. We'll create a blog API where:
- Free posts are accessible to everyone
- Paid posts require purchase for full content access
- JWT authentication secures user-specific endpoints
- Proper serialization handles data transformation
- Access control manages content visibility
By the end of this tutorial, you'll have a complete API that can handle real-world scenarios and scale with your application needs.
Project Overview
We're building a blog API with the following features:
Core Functionality
- Blog Posts: Free and paid content with access control
- Categories: Hierarchical organization with parent/child relationships
- Tags: Flexible content tagging system
- Users: Custom email-based authentication
- Orders: Purchase tracking for paid content
API Features
- JWT Authentication: Secure token-based authentication
- Access Control: Different permission levels for content
- Pagination: Handle large datasets efficiently
- Filtering & Search: Find content easily
- CORS Support: Enable frontend integration
Security Features
- Content Protection: Paid content only accessible to purchasers
- Token Refresh: Secure session management
- Permission Classes: Granular access control
- Input Validation: Secure data handling
Prerequisites
Before starting, ensure you have:
- Python 3.8+ installed on your system
- Basic Django knowledge (models, views, URLs)
- Understanding of REST principles (HTTP methods, status codes)
- Familiarity with JSON and API concepts
- Command line experience for running commands
Recommended Knowledge
- Git for version control
- Virtual environments in Python
- Database basics (we'll use SQLite for development)
- Postman or curl for API testing
Setting Up the Environment
1. Create Project Directory
mkdir django-blog-api
cd django-blog-api
2. Set Up Virtual Environment
# Create virtual environment
python -m venv .venv
# Activate virtual environment
# On Linux/Mac:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate
3. Create Django Project
# Install Django
pip install django
# Create project
django-admin startproject blogapi .
# Create necessary apps
python manage.py startapp accounts
python manage.py startapp blog
python manage.py startapp orders
python manage.py startapp api
4. Project Structure
Your project should look like this:
django-blog-api/
├── .venv/
├── blogapi/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── accounts/
├── blog/
├── orders/
├── api/
├── manage.py
└── requirements.txt
Installing Dependencies
1. Install Required Packages
pip install djangorestframework
pip install django-cors-headers
pip install djangorestframework-simplejwt
pip install pillow # For image handling
2. Create requirements.txt
pip freeze > requirements.txt
Your requirements.txt should include:
Django==5.0.1
djangorestframework==3.16.1
django-cors-headers==4.9.0
djangorestframework-simplejwt==5.5.1
pillow==10.2.0
Configuring Django Settings
1. Update settings.py
# blogapi/settings.py
from pathlib import Path
from datetime import timedelta
import os
BASE_DIR = Path(__file__).resolve().parent.parent
# Security settings
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'your-secret-key-here')
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'your-domain.com']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'rest_framework',
'corsheaders',
'rest_framework_simplejwt',
# Local apps
'accounts',
'blog',
'orders',
'api',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'blogapi.urls'
# Templates
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Custom User Model
AUTH_USER_MODEL = 'accounts.User'
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Django REST Framework Configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
# JWT Settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
# CORS Settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # React frontend
"http://127.0.0.1:3000",
"http://localhost:8080", # Vue frontend
]
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only for development
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Understanding the Models
Let's create the data models that will power our API.
1. Custom User Model (accounts/models.py)
# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
class CustomUserManager(BaseUserManager):
"""Custom user manager where email is the unique identifier."""
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The Email field must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
"""Custom user model with email as the unique identifier."""
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = 'user'
verbose_name_plural = 'users'
def __str__(self):
return self.email
def get_full_name(self):
return self.full_name or self.email
2. Blog Models (blog/models.py)
# blog/models.py
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
from django.conf import settings
class Category(models.Model):
"""Category model with parent/child relationships."""
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True, blank=True)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='subcategories'
)
order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
class Meta:
verbose_name_plural = 'Categories'
ordering = ['order', 'name']
def __str__(self):
if self.parent:
return f"{self.parent.name} > {self.name}"
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Tag(models.Model):
"""Tag model for categorizing blog posts."""
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True, blank=True)
color = models.CharField(max_length=20, default='indigo')
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Post(models.Model):
"""Blog post model with access control."""
POST_TYPE_CHOICES = [
('tutorial', 'Tutorial'),
('article', 'Article'),
]
SCHEMA_TYPE_CHOICES = [
('Article', 'Article'),
('BlogPosting', 'Blog Post'),
('TechArticle', 'Technical Article'),
('Tutorial', 'Tutorial'),
('HowTo', 'How-To Guide'),
]
# Basic fields
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True)
excerpt = models.TextField(max_length=500, help_text='Short summary for cards and SEO')
content = models.TextField(help_text='Write in Markdown format')
# SEO fields
seo_title = models.CharField(max_length=60, blank=True, null=True)
meta_description = models.TextField(max_length=160, blank=True, null=True)
meta_keywords = models.CharField(max_length=255, blank=True, null=True)
focus_keyword = models.CharField(max_length=100, blank=True, null=True)
canonical_url = models.URLField(blank=True, null=True)
og_image_alt = models.CharField(max_length=125, blank=True, null=True)
schema_type = models.CharField(
max_length=50,
choices=SCHEMA_TYPE_CHOICES,
default='BlogPosting'
)
reading_time_override = models.PositiveIntegerField(blank=True, null=True)
# Media
cover_image = models.ImageField(upload_to='blog/', blank=True, null=True)
# Publishing
is_published = models.BooleanField(default=False)
published_at = models.DateTimeField(blank=True, null=True)
is_featured = models.BooleanField(default=False)
# Access control
is_free = models.BooleanField(default=True)
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)
# Type and organization
post_type = models.CharField(max_length=20, choices=POST_TYPE_CHOICES, default='tutorial')
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts'
)
# Relations
tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='blog_posts'
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-published_at', '-created_at']
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
if self.is_published and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
@property
def reading_time(self):
"""Estimate reading time in minutes."""
if self.reading_time_override:
return self.reading_time_override
word_count = len(self.content.split())
minutes = max(1, round(word_count / 200))
return minutes
@property
def get_seo_title(self):
"""Return SEO title or fallback to main title."""
return self.seo_title or self.title
@property
def get_meta_description(self):
"""Return meta description or fallback to excerpt."""
return self.meta_description or self.excerpt
3. Order Model (orders/models.py)
# orders/models.py
from decimal import Decimal
from django.conf import settings
from django.db import models
from django.utils import timezone
class Order(models.Model):
"""Order model for tracking purchases."""
STATUS_PENDING = 'pending'
STATUS_PAID = 'paid'
STATUS_FAILED = 'failed'
STATUS_REFUNDED = 'refunded'
STATUS_CANCELLED = 'cancelled'
STATUS_CHOICES = [
(STATUS_PENDING, 'Pending'),
(STATUS_PAID, 'Paid'),
(STATUS_FAILED, 'Failed'),
(STATUS_REFUNDED, 'Refunded'),
(STATUS_CANCELLED, 'Cancelled'),
]
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='orders'
)
post = models.ForeignKey(
'blog.Post',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='orders'
)
order_number = models.CharField(max_length=20, unique=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
# Financial details
total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
paid_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"Order {self.order_number} - {self.user.email}"
def save(self, *args, **kwargs):
if not self.order_number:
self.order_number = f"ORD-{timezone.now().strftime('%Y%m%d%H%M%S')}"
if self.status == self.STATUS_PAID and not self.paid_at:
self.paid_at = timezone.now()
super().save(*args, **kwargs)
Creating API Serializers
Serializers convert Django model instances to JSON and vice versa.
1. Create api/serializers.py
# api/serializers.py
from rest_framework import serializers
from blog.models import Post, Category, Tag
from orders.models import Order
from django.contrib.auth import get_user_model
User = get_user_model()
class CategorySerializer(serializers.ModelSerializer):
"""Serializer for Category model"""
subcategories = serializers.SerializerMethodField()
posts_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'parent', 'subcategories', 'posts_count', 'is_active']
def get_subcategories(self, obj):
if obj.parent is None and obj.subcategories.exists():
return CategorySerializer(obj.subcategories.filter(is_active=True), many=True).data
return []
def get_posts_count(self, obj):
return obj.posts.filter(is_published=True).count()
class TagSerializer(serializers.ModelSerializer):
"""Serializer for Tag model"""
posts_count = serializers.SerializerMethodField()
class Meta:
model = Tag
fields = ['id', 'name', 'slug', 'color', 'posts_count']
def get_posts_count(self, obj):
return obj.posts.filter(is_published=True).count()
class AuthorSerializer(serializers.ModelSerializer):
"""Serializer for User model (as author)"""
class Meta:
model = User
fields = ['id', 'email', 'full_name']
class PostListSerializer(serializers.ModelSerializer):
"""Serializer for Post list view"""
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
author = AuthorSerializer(read_only=True)
reading_time = serializers.ReadOnlyField()
is_accessible = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'excerpt', 'cover_image',
'published_at', 'reading_time', 'is_featured', 'is_free', 'price',
'post_type', 'category', 'tags', 'author', 'is_accessible'
]
def get_is_accessible(self, obj):
"""Check if user can access this post"""
request = self.context.get('request')
if not request:
return obj.is_free
# If post is free, everyone can access
if obj.is_free:
return True
# If user is not authenticated, only free posts
if not request.user.is_authenticated:
return False
# Check if user has paid for this post
return Order.objects.filter(
user=request.user,
post=obj,
status='paid'
).exists()
class PostDetailSerializer(serializers.ModelSerializer):
"""Serializer for Post detail view"""
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
author = AuthorSerializer(read_only=True)
reading_time = serializers.ReadOnlyField()
get_seo_title = serializers.ReadOnlyField()
get_meta_description = serializers.ReadOnlyField()
content = serializers.SerializerMethodField()
is_accessible = serializers.SerializerMethodField()
class Meta:
model = Post
fields = [
'id', 'title', 'slug', 'excerpt', 'content', 'cover_image',
'published_at', 'updated_at', 'reading_time', 'is_featured',
'is_free', 'price', 'post_type', 'category', 'tags', 'author',
'get_seo_title', 'get_meta_description', 'is_accessible'
]
def get_content(self, obj):
"""Return content only if user has access"""
request = self.context.get('request')
# If post is free, return content
if obj.is_free:
return obj.content
# If user is not authenticated, return None
if not request or not request.user.is_authenticated:
return None
# Check if user has paid for this post
has_access = Order.objects.filter(
user=request.user,
post=obj,
status='paid'
).exists()
if has_access:
return obj.content
else:
# Return a teaser
return obj.content[:500] + "... [Content locked - Purchase required]"
def get_is_accessible(self, obj):
"""Check if user can access this post"""
request = self.context.get('request')
if not request:
return obj.is_free
if obj.is_free:
return True
if not request.user.is_authenticated:
return False
return Order.objects.filter(
user=request.user,
post=obj,
status='paid'
).exists()
Building API Views with Access Control
1. Create api/views.py
# api/views.py
from rest_framework import generics, permissions
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from django.db.models import Q
from blog.models import Post, Category, Tag
from orders.models import Order
from .serializers import (
PostListSerializer, PostDetailSerializer,
CategorySerializer, TagSerializer
)
class FreePostListView(generics.ListAPIView):
"""List all free blog posts - accessible to everyone"""
serializer_class = PostListSerializer
permission_classes = [permissions.AllowAny]
filterset_fields = ['category', 'post_type', 'is_featured']
search_fields = ['title', 'excerpt', 'content']
ordering_fields = ['published_at', 'title', 'reading_time']
ordering = ['-published_at']
def get_queryset(self):
return Post.objects.filter(
is_published=True,
is_free=True
).select_related('author', 'category').prefetch_related('tags')
class FreePostDetailView(generics.RetrieveAPIView):
"""Retrieve a free blog post - accessible to everyone"""
serializer_class = PostDetailSerializer
permission_classes = [permissions.AllowAny]
lookup_field = 'slug'
def get_queryset(self):
return Post.objects.filter(
is_published=True,
is_free=True
).select_related('author', 'category').prefetch_related('tags')
class PaidPostListView(generics.ListAPIView):
"""List all paid blog posts - content access controlled"""
serializer_class = PostListSerializer
permission_classes = [permissions.AllowAny]
filterset_fields = ['category', 'post_type', 'price']
search_fields = ['title', 'excerpt']
ordering_fields = ['published_at', 'title', 'price', 'reading_time']
ordering = ['-published_at']
def get_queryset(self):
return Post.objects.filter(
is_published=True,
is_free=False
).select_related('author', 'category').prefetch_related('tags')
class PaidPostDetailView(generics.RetrieveAPIView):
"""Retrieve a paid blog post - content access controlled"""
serializer_class = PostDetailSerializer
permission_classes = [permissions.AllowAny]
lookup_field = 'slug'
def get_queryset(self):
return Post.objects.filter(
is_published=True,
is_free=False
).select_related('author', 'category').prefetch_related('tags')
class AllPostListView(generics.ListAPIView):
"""List all blog posts (free and paid) - content access controlled"""
serializer_class = PostListSerializer
permission_classes = [permissions.AllowAny]
filterset_fields = ['category', 'post_type', 'is_free', 'is_featured']
search_fields = ['title', 'excerpt']
ordering_fields = ['published_at', 'title', 'price', 'reading_time']
ordering = ['-published_at']
def get_queryset(self):
return Post.objects.filter(
is_published=True
).select_related('author', 'category').prefetch_related('tags')
class CategoryListView(generics.ListAPIView):
"""List all active categories"""
serializer_class = CategorySerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Category.objects.filter(is_active=True).prefetch_related('subcategories')
class TagListView(generics.ListAPIView):
"""List all tags"""
serializer_class = TagSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Tag.objects.all()
@api_view(['GET'])
@permission_classes([permissions.AllowAny])
def post_search(request):
"""Search posts by title, excerpt, or content"""
query = request.GET.get('q', '')
post_type = request.GET.get('type', '')
if not query:
return Response({'results': []})
posts = Post.objects.filter(is_published=True)
if post_type == 'free':
posts = posts.filter(is_free=True)
elif post_type == 'paid':
posts = posts.filter(is_free=False)
posts = posts.filter(
Q(title__icontains=query) |
Q(excerpt__icontains=query) |
Q(content__icontains=query)
).select_related('author', 'category').prefetch_related('tags')[:20]
serializer = PostListSerializer(posts, many=True, context={'request': request})
return Response({'results': serializer.data})
@api_view(['GET'])
@permission_classes([permissions.AllowAny])
def featured_posts(request):
"""Get featured posts"""
posts = Post.objects.filter(
is_published=True,
is_featured=True
).select_related('author', 'category').prefetch_related('tags')[:10]
serializer = PostListSerializer(posts, many=True, context={'request': request})
return Response({'results': serializer.data})
Implementing JWT Authentication
1. Create api/auth_views.py
# api/auth_views.py
from rest_framework import status, serializers
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
User = get_user_model()
class UserRegistrationSerializer(serializers.ModelSerializer):
"""Serializer for user registration"""
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['email', 'password', 'password_confirm', 'full_name']
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError("Passwords don't match")
return attrs
def create(self, validated_data):
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user
class UserSerializer(serializers.ModelSerializer):
"""Serializer for user data"""
class Meta:
model = User
fields = ['id', 'email', 'full_name', 'date_joined']
@api_view(['POST'])
@permission_classes([AllowAny])
def register(request):
"""Register a new user"""
serializer = UserRegistrationSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
# Generate tokens
refresh = RefreshToken.for_user(user)
return Response({
'user': UserSerializer(user).data,
'tokens': {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
"""Login user and return JWT tokens"""
email = request.data.get('email')
password = request.data.get('password')
if not email or not password:
return Response({
'error': 'Email and password are required'
}, status=status.HTTP_400_BAD_REQUEST)
user = authenticate(request, username=email, password=password)
if user:
refresh = RefreshToken.for_user(user)
return Response({
'user': UserSerializer(user).data,
'tokens': {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
})
return Response({
'error': 'Invalid credentials'
}, status=status.HTTP_401_UNAUTHORIZED)
@api_view(['POST'])
@permission_classes([AllowAny])
def refresh_token(request):
"""Refresh JWT token"""
refresh_token = request.data.get('refresh')
if not refresh_token:
return Response({
'error': 'Refresh token is required'
}, status=status.HTTP_400_BAD_REQUEST)
try:
refresh = RefreshToken(refresh_token)
return Response({
'access': str(refresh.access_token)
})
except Exception:
return Response({
'error': 'Invalid refresh token'
}, status=status.HTTP_401_UNAUTHORIZED)
URL Configuration
1. Create api/urls.py
# api/urls.py
from django.urls import path
from . import views, auth_views
app_name = 'api'
urlpatterns = [
# Authentication endpoints
path('auth/register/', auth_views.register, name='register'),
path('auth/login/', auth_views.login, name='login'),
path('auth/refresh/', auth_views.refresh_token, name='refresh_token'),
# Free blog posts
path('posts/free/', views.FreePostListView.as_view(), name='free_posts'),
path('posts/free/<slug:slug>/', views.FreePostDetailView.as_view(), name='free_post_detail'),
# Paid blog posts
path('posts/paid/', views.PaidPostListView.as_view(), name='paid_posts'),
path('posts/paid/<slug:slug>/', views.PaidPostDetailView.as_view(), name='paid_post_detail'),
# All blog posts
path('posts/', views.AllPostListView.as_view(), name='all_posts'),
# Categories and tags
path('categories/', views.CategoryListView.as_view(), name='categories'),
path('tags/', views.TagListView.as_view(), name='tags'),
# Search and special endpoints
path('posts/search/', views.post_search, name='post_search'),
path('posts/featured/', views.featured_posts, name='featured_posts'),
]
2. Update main urls.py
# blogapi/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Testing the API
1. Run Migrations
# Create and apply migrations
python manage.py makemigrations accounts
python manage.py makemigrations blog
python manage.py makemigrations orders
python manage.py migrate
2. Create Superuser
python manage.py createsuperuser
3. Start Development Server
python manage.py runserver
4. Test Endpoints
Test Free Posts
curl -X GET "http://localhost:8000/api/v1/posts/free/" \
-H "Accept: application/json"
Test User Registration
curl -X POST "http://localhost:8000/api/v1/auth/register/" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "securepassword123",
"password_confirm": "securepassword123",
"full_name": "Test User"
}'
Test User Login
curl -X POST "http://localhost:8000/api/v1/auth/login/" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "securepassword123"
}'
5. Create Test Data
Create some blog posts through Django admin or create a management command:
# blog/management/commands/create_sample_data.py
from django.core.management.base import BaseCommand
from blog.models import Category, Tag, Post
from accounts.models import User
class Command(BaseCommand):
def handle(self, *args, **options):
# Create categories
web_dev = Category.objects.create(name="Web Development", slug="web-development")
django_cat = Category.objects.create(
name="Django",
slug="django",
parent=web_dev
)
# Create tags
django_tag = Tag.objects.create(name="Django", color="green")
python_tag = Tag.objects.create(name="Python", color="blue")
# Get or create author
author, _ = User.objects.get_or_create(
email="author@example.com",
defaults={'full_name': 'Blog Author'}
)
# Create free post
free_post = Post.objects.create(
title="Getting Started with Django",
slug="getting-started-django",
excerpt="Learn the basics of Django web framework",
content="# Getting Started with Django\n\nDjango is awesome...",
is_published=True,
is_free=True,
category=django_cat,
author=author
)
free_post.tags.add(django_tag, python_tag)
# Create paid post
paid_post = Post.objects.create(
title="Advanced Django Patterns",
slug="advanced-django-patterns",
excerpt="Master advanced Django development",
content="# Advanced Django Patterns\n\nThis covers advanced topics...",
is_published=True,
is_free=False,
price=29.99,
category=django_cat,
author=author
)
paid_post.tags.add(django_tag)
self.stdout.write("Sample data created successfully!")
Run the command:
python manage.py create_sample_data
Production Deployment
1. Environment Variables
Create a .env file:
DJANGO_SECRET_KEY=your-super-secret-key-here
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgres://user:pass@localhost/dbname
2. Update Settings for Production
# blogapi/settings.py
import os
from dotenv import load_dotenv
load_dotenv()
# Security settings
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = os.getenv('DJANGO_DEBUG', 'False').lower() in ('true', '1')
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '').split(',')
# Database
if os.getenv('DATABASE_URL'):
import dj_database_url
DATABASES = {
'default': dj_database_url.parse(os.getenv('DATABASE_URL'))
}
# Static files for production
STATIC_ROOT = BASE_DIR / 'staticfiles'
# CORS settings for production
CORS_ALLOWED_ORIGINS = [
"https://yourdomain.com",
"https://www.yourdomain.com",
]
CORS_ALLOW_ALL_ORIGINS = DEBUG
3. Requirements for Production
# requirements.txt
Django==5.0.1
djangorestframework==3.16.1
django-cors-headers==4.9.0
djangorestframework-simplejwt==5.5.1
pillow==10.2.0
python-dotenv==1.0.0
dj-database-url==2.1.0
psycopg2-binary==2.9.9
gunicorn==21.2.0
4. Deploy to Production
# Collect static files
python manage.py collectstatic --noinput
# Run with Gunicorn
gunicorn blogapi.wsgi:application --bind 0.0.0.0:8000
Best Practices
1. Security
- Always use HTTPS in production
- Keep SECRET_KEY secure and random
- Use environment variables for sensitive data
- Implement rate limiting
- Validate all user inputs
- Use strong password policies
2. Performance
- Use
select_related()andprefetch_related()for database optimization - Implement caching for frequently accessed data
- Use database indexes on filtered fields
- Optimize image sizes and formats
- Monitor API response times
3. Code Organization
- Keep serializers focused and simple
- Use custom permissions for complex access control
- Write comprehensive tests
- Document your API endpoints
- Use consistent naming conventions
4. API Design
- Follow RESTful conventions
- Use appropriate HTTP status codes
- Implement proper pagination
- Provide meaningful error messages
- Version your APIs
Troubleshooting
Common Issues
1. CORS Errors
# Add to settings.py
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://yourdomain.com",
]
2. JWT Token Issues
# Check token in request headers
Authorization: Bearer your_jwt_token_here
3. Database Errors
# Reset migrations if needed
python manage.py migrate --fake-initial
4. Import Errors
# Check app registration in INSTALLED_APPS
INSTALLED_APPS = [
# ... other apps
'api',
]
Debugging Tips
- Use Django Debug Toolbar for development
- Check server logs for detailed error information
- Test with curl or Postman before frontend integration
- Use Django shell to test model queries
- Enable detailed logging for production issues
Conclusion
Congratulations! You've built a comprehensive Django REST API with:
✅ JWT Authentication - Secure token-based authentication
✅ Access Control - Different permission levels for content
✅ Custom User Model - Email-based authentication
✅ Content Management - Blog posts with categories and tags
✅ Purchase System - Paid content with access control
✅ Production Ready - CORS, security, and deployment configuration
What You've Learned
- Django REST Framework fundamentals
- JWT authentication implementation
- Custom serializers with access control
- Permission classes and security
- Database relationships and optimization
- API design best practices
- Production deployment considerations
Next Steps
- Add email verification for user registration
- Implement password reset functionality
- Add payment processing for paid content
- Create API documentation with tools like Swagger
- Add comprehensive testing with unit and integration tests
- Implement caching for better performance
- Add API versioning for future updates
Resources
- Django REST Framework Documentation
- JWT Authentication Guide
- Django Best Practices
- API Design Guidelines
This tutorial provides a solid foundation for building production-ready APIs. You can extend this codebase for various applications like e-commerce platforms, content management systems, or mobile app backends.
Happy coding!
About the Author: This tutorial was created by the Amstack development team. For more Django tutorials and web development resources, visit our blog.
Last Updated: January 2026
Django Version: 5.0+
Python Version: 3.8+