Introduction
APIs are a two-way street. While returning JSON responses is essential, equally important is receiving and processing data from clients. Whether it's a user submitting a form, a mobile app uploading a photo, or a service sending webhook data, your API needs to handle incoming data safely and efficiently.
Key Concepts
Request Body: The payload sent with POST, PUT, and PATCH requests. In REST APIs, this is typically JSON data accessed via request.body.
Query Parameters: Key-value pairs in the URL (e.g., ?page=2&search=django), accessed via request.GET.
Request Headers: Metadata sent with requests like Authorization, Content-Type, and custom headers, accessed via request.headers.
CSRF (Cross-Site Request Forgery): Django's built-in protection against malicious requests. API endpoints often use token authentication instead.
Real World Context
Every production API handles:
- User registration: Parsing email, password, profile data from POST requests
- Search and filtering: Reading query parameters for pagination, sorting, filtering
- File uploads: Processing multipart form data
- Webhooks: Receiving and validating data from external services (Stripe, GitHub, etc.)
Poor request handling leads to security vulnerabilities, data corruption, and frustrating error messages for API consumers.
Deep Dive
Parsing JSON Request Body
This view demonstrates the full pattern for safely receiving JSON data: checking the HTTP method, parsing the body, validating fields, and returning appropriate status codes:
pythonimport json from django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_exempt @csrf_exempt # Use token auth instead for APIs def api_create_article(request): if request.method != 'POST': return JsonResponse({'error': 'Method not allowed'}, status=405) # Parse JSON body try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) # Validate required fields if 'title' not in data: return JsonResponse( {'error': 'Validation failed', 'details': {'title': 'This field is required'}}, status=400 ) # Create the resource article = Article.objects.create( title=data['title'], body=data.get('body', ''), author=request.user ) return JsonResponse({'id': article.id, 'title': article.title}, status=201)
The @csrf_exempt decorator disables CSRF protection for this view since API clients typically use token authentication instead. The json.loads(request.body) call is wrapped in a try/except to handle malformed JSON gracefully.
Query Parameters
Query parameters are read from request.GET and are ideal for filtering, pagination, and sorting. This example shows how to parse, validate, and apply multiple query parameters:
pythondef api_articles(request): # GET /api/articles?page=2&per_page=10&search=django&sort=-created_at # Parse with defaults and type conversion try: page = int(request.GET.get('page', 1)) per_page = min(int(request.GET.get('per_page', 20)), 100) # Cap at 100 except ValueError: return JsonResponse({'error': 'Invalid pagination parameters'}, status=400) search = request.GET.get('search', '').strip() sort = request.GET.get('sort', '-created_at') # Build queryset articles = Article.objects.all() if search: articles = articles.filter(title__icontains=search) # Validate sort field allowed_sorts = ['created_at', '-created_at', 'title', '-title'] if sort in allowed_sorts: articles = articles.order_by(sort) # Paginate start = (page - 1) * per_page total = articles.count() articles = articles[start:start + per_page] return JsonResponse({ 'data': list(articles.values('id', 'title', 'created_at')), 'meta': {'page': page, 'per_page': per_page, 'total': total} })
Notice the use of min() to cap per_page at 100, preventing clients from requesting excessive data. Always provide defaults with request.GET.get() and handle type conversion errors.
Complete CRUD View
Here is a single view function that handles all CRUD operations by branching on request.method. This pattern keeps related logic together:
python@csrf_exempt def api_article(request, pk=None): if request.method == 'GET': if pk: article = get_object_or_404(Article, pk=pk) return JsonResponse({'id': article.id, 'title': article.title, 'body': article.body}) articles = Article.objects.all()[:20] return JsonResponse({'data': list(articles.values('id', 'title'))}) elif request.method == 'POST': try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) article = Article.objects.create(title=data['title'], body=data.get('body', '')) return JsonResponse({'id': article.id}, status=201) elif request.method == 'PUT': # Full replacement article = get_object_or_404(Article, pk=pk) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) article.title = data['title'] # Required article.body = data['body'] # Required article.save() return JsonResponse({'id': article.id}) elif request.method == 'PATCH': # Partial update article = get_object_or_404(Article, pk=pk) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) if 'title' in data: article.title = data['title'] if 'body' in data: article.body = data['body'] article.save() return JsonResponse({'id': article.id}) elif request.method == 'DELETE': article = get_object_or_404(Article, pk=pk) article.delete() return HttpResponse(status=204) return JsonResponse({'error': 'Method not allowed'}, status=405)
The key difference between PUT and PATCH is that PUT requires all fields (full replacement), while PATCH only updates the fields present in the request body. DELETE returns status 204 with no body.
Reading Request Headers
Django exposes request headers through request.headers, a case-insensitive dictionary. This is useful for reading authentication tokens, content types, and custom headers:
pythondef api_protected(request): # Access headers (case-insensitive) auth = request.headers.get('Authorization', '') content_type = request.headers.get('Content-Type') api_key = request.headers.get('X-API-Key') if not api_key or api_key != settings.API_KEY: return JsonResponse({'error': 'Invalid API key'}, status=401)
The request.headers dict was introduced in Django 2.2. Custom headers like X-API-Key follow the same access pattern as standard ones.
Common Pitfalls
-
Not handling JSONDecodeError: Always wrap
json.loads()in try/except. Malformed JSON should return 400, not 500. -
Type coercion failures:
int(request.GET.get('page'))crashes if page is missing or not a number. Always provide defaults and handle ValueError. -
Trusting client data: Never use
data.get('is_admin', False)to set permissions. Validate and sanitize all input against your business rules.
Best Practices
-
Validate early, fail fast: Check required fields before any database operations.
-
Return helpful error messages: Include field-level details:
{'errors': {'email': 'Invalid format'}}. -
Cap pagination limits: Prevent
?per_page=1000000from killing your server. -
Whitelist allowed query parameters: Don't let users sort or filter by arbitrary fields.
-
Use consistent parameter names: Stick to
page/per_pageoroffset/limitacross all endpoints.
Summary
Handling request data involves parsing JSON bodies with json.loads(request.body), reading query parameters from request.GET, and accessing headers via request.headers. Always validate input, handle parsing errors gracefully, and return meaningful error messages. Remember that PUT expects complete resource replacement while PATCH handles partial updates.
Code Examples
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def api_create_article(request):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
if 'title' not in data:
return JsonResponse({'error': 'title is required'}, status=400)
article = Article.objects.create(title=data['title'])
return JsonResponse({'id': article.id}, status=201)