URLs and Views

Handling HTTP Methods

HTTP methods define the type of action being performed on a resource. Django provides comprehensive support for handling different HTTP methods, enabling you to build RESTful APIs and implement proper request handling patterns.

Handling HTTP Methods

HTTP methods define the type of action being performed on a resource. Django provides comprehensive support for handling different HTTP methods, enabling you to build RESTful APIs and implement proper request handling patterns.

HTTP Method Overview

Standard HTTP Methods

from django.views.decorators.http import require_http_methods, require_GET, require_POST, require_safe
from django.http import JsonResponse, HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required

# GET - Retrieve data (safe, idempotent)
@require_GET
def get_posts(request):
    """Retrieve list of posts"""
    posts = Post.objects.filter(status='published').order_by('-created_at')
    
    # Handle query parameters
    search = request.GET.get('search', '')
    category = request.GET.get('category')
    page = int(request.GET.get('page', 1))
    
    if search:
        posts = posts.filter(title__icontains=search)
    
    if category:
        posts = posts.filter(category__slug=category)
    
    # Pagination
    from django.core.paginator import Paginator
    paginator = Paginator(posts, 10)
    posts_page = paginator.get_page(page)
    
    return render(request, 'blog/post_list.html', {
        'posts': posts_page,
        'search': search,
        'category': category
    })

# POST - Create new resource
@require_POST
@login_required
def create_post(request):
    """Create new post"""
    form = PostForm(request.POST, request.FILES)
    
    if form.is_valid():
        post = form.save(commit=False)
        post.author = request.user
        post.save()
        form.save_m2m()
        
        return JsonResponse({
            'success': True,
            'post_id': post.id,
            'redirect_url': post.get_absolute_url()
        })
    else:
        return JsonResponse({
            'success': False,
            'errors': form.errors
        }, status=400)

# PUT - Update entire resource
@require_http_methods(["PUT"])
@login_required
def update_post(request, pk):
    """Update entire post"""
    post = get_object_or_404(Post, pk=pk)
    
    # Check permissions
    if post.author != request.user and not request.user.is_staff:
        return JsonResponse({'error': 'Permission denied'}, status=403)
    
    try:
        import json
        data = json.loads(request.body)
        
        # Update all fields
        post.title = data.get('title', post.title)
        post.content = data.get('content', post.content)
        post.status = data.get('status', post.status)
        
        # Validate and save
        post.full_clean()
        post.save()
        
        return JsonResponse({
            'success': True,
            'message': 'Post updated successfully'
        })
        
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

# PATCH - Partial update
@require_http_methods(["PATCH"])
@login_required
def partial_update_post(request, pk):
    """Partially update post"""
    post = get_object_or_404(Post, pk=pk)
    
    if post.author != request.user and not request.user.is_staff:
        return JsonResponse({'error': 'Permission denied'}, status=403)
    
    try:
        import json
        data = json.loads(request.body)
        
        # Update only provided fields
        updated_fields = []
        
        if 'title' in data:
            post.title = data['title']
            updated_fields.append('title')
        
        if 'content' in data:
            post.content = data['content']
            updated_fields.append('content')
        
        if 'status' in data:
            post.status = data['status']
            updated_fields.append('status')
        
        if updated_fields:
            post.save(update_fields=updated_fields)
        
        return JsonResponse({
            'success': True,
            'updated_fields': updated_fields
        })
        
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)

# DELETE - Remove resource
@require_http_methods(["DELETE"])
@login_required
def delete_post(request, pk):
    """Delete post"""
    post = get_object_or_404(Post, pk=pk)
    
    if post.author != request.user and not request.user.is_staff:
        return JsonResponse({'error': 'Permission denied'}, status=403)
    
    post.delete()
    
    return JsonResponse({
        'success': True,
        'message': 'Post deleted successfully'
    }, status=204)

# HEAD - Get headers only (like GET but no body)
@require_http_methods(["HEAD", "GET"])
def post_head(request, pk):
    """Handle HEAD requests for post metadata"""
    post = get_object_or_404(Post, pk=pk, status='published')
    
    response = HttpResponse()
    response['Content-Type'] = 'text/html'
    response['Content-Length'] = len(post.content)
    response['Last-Modified'] = post.updated_at.strftime('%a, %d %b %Y %H:%M:%S GMT')
    response['ETag'] = f'"{post.id}-{post.updated_at.timestamp()}"'
    
    if request.method == 'GET':
        # Return full content for GET
        return render(request, 'blog/post_detail.html', {'post': post})
    
    # Return empty response for HEAD
    return response

# OPTIONS - Get allowed methods
@require_http_methods(["OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"])
def post_options(request, pk=None):
    """Handle OPTIONS requests"""
    if request.method == 'OPTIONS':
        response = HttpResponse()
        
        if pk:
            # Specific resource
            response['Allow'] = 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS'
        else:
            # Collection
            response['Allow'] = 'GET, POST, HEAD, OPTIONS'
        
        response['Access-Control-Allow-Methods'] = response['Allow']
        response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
        
        return response
    
    # Handle other methods...
    return JsonResponse({'message': 'Method handling here'})

RESTful View Patterns

Resource-Based Views

from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json

@csrf_exempt
def post_api_view(request, pk=None):
    """RESTful API endpoint for posts"""
    
    if request.method == 'GET':
        if pk:
            # GET /api/posts/1/ - Retrieve specific post
            post = get_object_or_404(Post, pk=pk, status='published')
            
            data = {
                'id': post.id,
                'title': post.title,
                'content': post.content,
                'author': {
                    'id': post.author.id,
                    'username': post.author.username
                },
                'created_at': post.created_at.isoformat(),
                'updated_at': post.updated_at.isoformat()
            }
            
            return JsonResponse(data)
        else:
            # GET /api/posts/ - List posts
            posts = Post.objects.filter(status='published').select_related('author')
            
            # Pagination
            page = int(request.GET.get('page', 1))
            per_page = min(int(request.GET.get('per_page', 10)), 100)
            
            from django.core.paginator import Paginator
            paginator = Paginator(posts, per_page)
            posts_page = paginator.get_page(page)
            
            data = {
                'posts': [
                    {
                        'id': post.id,
                        'title': post.title,
                        'author': post.author.username,
                        'created_at': post.created_at.isoformat()
                    }
                    for post in posts_page.object_list
                ],
                'pagination': {
                    'current_page': posts_page.number,
                    'total_pages': paginator.num_pages,
                    'total_count': paginator.count,
                    'has_next': posts_page.has_next(),
                    'has_previous': posts_page.has_previous()
                }
            }
            
            return JsonResponse(data)
    
    elif request.method == 'POST':
        # POST /api/posts/ - Create new post
        if not request.user.is_authenticated:
            return JsonResponse({'error': 'Authentication required'}, status=401)
        
        try:
            data = json.loads(request.body)
            
            # Validate required fields
            if not data.get('title') or not data.get('content'):
                return JsonResponse({
                    'error': 'Title and content are required'
                }, status=400)
            
            # Create post
            post = Post.objects.create(
                title=data['title'],
                content=data['content'],
                author=request.user,
                status=data.get('status', 'draft')
            )
            
            return JsonResponse({
                'id': post.id,
                'title': post.title,
                'created_at': post.created_at.isoformat()
            }, status=201)
            
        except json.JSONDecodeError:
            return JsonResponse({'error': 'Invalid JSON'}, status=400)
        except Exception as e:
            return JsonResponse({'error': str(e)}, status=500)
    
    elif request.method == 'PUT':
        # PUT /api/posts/1/ - Update entire post
        if not pk:
            return JsonResponse({'error': 'Post ID required for PUT'}, status=400)
        
        post = get_object_or_404(Post, pk=pk)
        
        if post.author != request.user and not request.user.is_staff:
            return JsonResponse({'error': 'Permission denied'}, status=403)
        
        try:
            data = json.loads(request.body)
            
            # Update all fields
            post.title = data.get('title', '')
            post.content = data.get('content', '')
            post.status = data.get('status', 'draft')
            
            post.full_clean()
            post.save()
            
            return JsonResponse({
                'id': post.id,
                'title': post.title,
                'updated_at': post.updated_at.isoformat()
            })
            
        except json.JSONDecodeError:
            return JsonResponse({'error': 'Invalid JSON'}, status=400)
        except ValidationError as e:
            return JsonResponse({'error': str(e)}, status=400)
    
    elif request.method == 'PATCH':
        # PATCH /api/posts/1/ - Partial update
        if not pk:
            return JsonResponse({'error': 'Post ID required for PATCH'}, status=400)
        
        post = get_object_or_404(Post, pk=pk)
        
        if post.author != request.user and not request.user.is_staff:
            return JsonResponse({'error': 'Permission denied'}, status=403)
        
        try:
            data = json.loads(request.body)
            updated_fields = []
            
            for field in ['title', 'content', 'status']:
                if field in data:
                    setattr(post, field, data[field])
                    updated_fields.append(field)
            
            if updated_fields:
                post.save(update_fields=updated_fields)
            
            return JsonResponse({
                'id': post.id,
                'updated_fields': updated_fields,
                'updated_at': post.updated_at.isoformat()
            })
            
        except json.JSONDecodeError:
            return JsonResponse({'error': 'Invalid JSON'}, status=400)
    
    elif request.method == 'DELETE':
        # DELETE /api/posts/1/ - Delete post
        if not pk:
            return JsonResponse({'error': 'Post ID required for DELETE'}, status=400)
        
        post = get_object_or_404(Post, pk=pk)
        
        if post.author != request.user and not request.user.is_staff:
            return JsonResponse({'error': 'Permission denied'}, status=403)
        
        post.delete()
        
        return JsonResponse({'message': 'Post deleted successfully'}, status=204)
    
    else:
        # Method not allowed
        return HttpResponseNotAllowed(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])

Form Handling with Multiple Methods

@require_http_methods(["GET", "POST"])
def post_form_view(request, pk=None):
    """Handle both display and submission of post form"""
    
    # Determine if this is create or edit
    if pk:
        post = get_object_or_404(Post, pk=pk)
        # Check edit permissions
        if post.author != request.user and not request.user.is_staff:
            return HttpResponseForbidden("You don't have permission to edit this post")
    else:
        post = None
    
    if request.method == 'GET':
        # Display form
        if post:
            form = PostForm(instance=post)
            template_context = {
                'form': form,
                'post': post,
                'is_edit': True
            }
        else:
            form = PostForm()
            template_context = {
                'form': form,
                'is_edit': False
            }
        
        return render(request, 'blog/post_form.html', template_context)
    
    elif request.method == 'POST':
        # Process form submission
        if post:
            form = PostForm(request.POST, request.FILES, instance=post)
        else:
            form = PostForm(request.POST, request.FILES)
        
        if form.is_valid():
            post = form.save(commit=False)
            
            if not post.author_id:
                post.author = request.user
            
            # Handle different submit buttons
            if 'save_draft' in request.POST:
                post.status = 'draft'
                messages.success(request, 'Post saved as draft.')
            elif 'publish' in request.POST:
                post.status = 'published'
                messages.success(request, 'Post published successfully!')
            elif 'save_continue' in request.POST:
                post.save()
                form.save_m2m()
                messages.success(request, 'Post saved. Continue editing.')
                return redirect('blog:edit_post', pk=post.pk)
            
            post.save()
            form.save_m2m()
            
            return redirect('blog:post_detail', pk=post.pk)
        else:
            # Form has errors
            messages.error(request, 'Please correct the errors below.')
            
            template_context = {
                'form': form,
                'post': post,
                'is_edit': bool(post)
            }
            
            return render(request, 'blog/post_form.html', template_context)

@require_http_methods(["POST", "DELETE"])
@login_required
def post_delete_view(request, pk):
    """Handle post deletion with confirmation"""
    post = get_object_or_404(Post, pk=pk)
    
    if post.author != request.user and not request.user.is_staff:
        return HttpResponseForbidden("You don't have permission to delete this post")
    
    if request.method == 'POST':
        # Show confirmation page
        return render(request, 'blog/post_confirm_delete.html', {'post': post})
    
    elif request.method == 'DELETE':
        # Actually delete the post
        post_title = post.title
        post.delete()
        
        # Handle different response formats
        if request.headers.get('Accept') == 'application/json':
            return JsonResponse({
                'success': True,
                'message': f'Post "{post_title}" deleted successfully'
            })
        else:
            messages.success(request, f'Post "{post_title}" deleted successfully.')
            return redirect('blog:post_list')

Advanced HTTP Method Handling

Custom Method Decorators

from functools import wraps
from django.http import HttpResponseNotAllowed, JsonResponse

def allow_methods(*methods):
    """Custom decorator to allow specific HTTP methods"""
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if request.method not in methods:
                if request.headers.get('Accept') == 'application/json':
                    return JsonResponse({
                        'error': f'Method {request.method} not allowed',
                        'allowed_methods': list(methods)
                    }, status=405)
                else:
                    return HttpResponseNotAllowed(methods)
            
            return view_func(request, *args, **kwargs)
        
        # Set allowed methods for OPTIONS handling
        wrapper.allowed_methods = methods
        return wrapper
    
    return decorator

def method_dispatcher(**method_handlers):
    """Decorator to dispatch different methods to different functions"""
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            method = request.method.lower()
            
            if method in method_handlers:
                return method_handlers[method](request, *args, **kwargs)
            else:
                # Fall back to original view function
                return view_func(request, *args, **kwargs)
        
        return wrapper
    
    return decorator

# Usage examples
@allow_methods('GET', 'POST', 'PUT', 'DELETE')
def api_endpoint(request):
    """API endpoint with custom method validation"""
    if request.method == 'GET':
        return JsonResponse({'message': 'GET request'})
    elif request.method == 'POST':
        return JsonResponse({'message': 'POST request'})
    # ... handle other methods

def handle_get(request, *args, **kwargs):
    return JsonResponse({'method': 'GET'})

def handle_post(request, *args, **kwargs):
    return JsonResponse({'method': 'POST'})

def handle_put(request, *args, **kwargs):
    return JsonResponse({'method': 'PUT'})

@method_dispatcher(
    get=handle_get,
    post=handle_post,
    put=handle_put
)
def dispatched_view(request):
    """Default handler for unspecified methods"""
    return JsonResponse({'method': 'DEFAULT'})

Content Type Handling

import json
from django.http import JsonResponse, HttpResponseBadRequest

def handle_content_types(request):
    """Handle different content types in requests"""
    
    if request.method == 'POST':
        content_type = request.content_type.lower()
        
        if content_type == 'application/json':
            # Handle JSON data
            try:
                data = json.loads(request.body)
                return JsonResponse({
                    'message': 'JSON data received',
                    'data': data
                })
            except json.JSONDecodeError:
                return JsonResponse({'error': 'Invalid JSON'}, status=400)
        
        elif content_type == 'application/x-www-form-urlencoded':
            # Handle form data
            data = dict(request.POST)
            return JsonResponse({
                'message': 'Form data received',
                'data': data
            })
        
        elif content_type.startswith('multipart/form-data'):
            # Handle multipart form (with files)
            files = {name: file.name for name, file in request.FILES.items()}
            data = dict(request.POST)
            
            return JsonResponse({
                'message': 'Multipart data received',
                'data': data,
                'files': files
            })
        
        elif content_type == 'text/plain':
            # Handle plain text
            text_data = request.body.decode('utf-8')
            return JsonResponse({
                'message': 'Text data received',
                'data': text_data
            })
        
        elif content_type == 'application/xml' or content_type == 'text/xml':
            # Handle XML data
            xml_data = request.body.decode('utf-8')
            return JsonResponse({
                'message': 'XML data received',
                'data': xml_data
            })
        
        else:
            return JsonResponse({
                'error': f'Unsupported content type: {content_type}'
            }, status=415)
    
    return JsonResponse({'message': 'Send POST request with data'})

def flexible_api_endpoint(request):
    """API endpoint that handles multiple content types and methods"""
    
    def parse_request_data():
        """Parse request data based on content type"""
        if request.method in ['POST', 'PUT', 'PATCH']:
            content_type = request.content_type.lower()
            
            if 'application/json' in content_type:
                return json.loads(request.body)
            elif 'application/x-www-form-urlencoded' in content_type:
                return dict(request.POST)
            elif 'multipart/form-data' in content_type:
                data = dict(request.POST)
                data.update({name: file for name, file in request.FILES.items()})
                return data
        
        return {}
    
    try:
        data = parse_request_data()
        
        if request.method == 'GET':
            return JsonResponse({'message': 'GET request', 'params': dict(request.GET)})
        
        elif request.method == 'POST':
            # Create resource
            return JsonResponse({'message': 'Created', 'data': data}, status=201)
        
        elif request.method == 'PUT':
            # Update resource
            return JsonResponse({'message': 'Updated', 'data': data})
        
        elif request.method == 'PATCH':
            # Partial update
            return JsonResponse({'message': 'Partially updated', 'data': data})
        
        elif request.method == 'DELETE':
            # Delete resource
            return JsonResponse({'message': 'Deleted'}, status=204)
        
        else:
            return HttpResponseNotAllowed(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
    
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)

CORS and Preflight Handling

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def cors_enabled_api(request):
    """API endpoint with CORS support"""
    
    # Handle preflight OPTIONS request
    if request.method == 'OPTIONS':
        response = HttpResponse()
        response['Access-Control-Allow-Origin'] = '*'
        response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
        response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
        response['Access-Control-Max-Age'] = '86400'  # 24 hours
        return response
    
    # Handle actual request
    if request.method == 'GET':
        data = {'message': 'CORS-enabled GET response'}
    elif request.method == 'POST':
        data = {'message': 'CORS-enabled POST response'}
    else:
        data = {'message': f'CORS-enabled {request.method} response'}
    
    response = JsonResponse(data)
    
    # Add CORS headers to actual response
    response['Access-Control-Allow-Origin'] = '*'
    response['Access-Control-Allow-Credentials'] = 'true'
    
    return response

def cors_middleware_example(get_response):
    """Example CORS middleware"""
    def middleware(request):
        response = get_response(request)
        
        # Add CORS headers to all responses
        response['Access-Control-Allow-Origin'] = '*'
        response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
        response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
        
        return response
    
    return middleware

Testing HTTP Methods

# tests/test_http_methods.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
import json

class HTTPMethodTests(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass'
        )
        self.post = Post.objects.create(
            title='Test Post',
            content='Test content',
            author=self.user
        )
    
    def test_get_request(self):
        """Test GET request handling"""
        response = self.client.get(f'/api/posts/{self.post.pk}/')
        
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data['title'], 'Test Post')
    
    def test_post_request(self):
        """Test POST request handling"""
        self.client.login(username='testuser', password='testpass')
        
        post_data = {
            'title': 'New Post',
            'content': 'New content'
        }
        
        response = self.client.post('/api/posts/', 
                                  json.dumps(post_data),
                                  content_type='application/json')
        
        self.assertEqual(response.status_code, 201)
        data = response.json()
        self.assertEqual(data['title'], 'New Post')
    
    def test_put_request(self):
        """Test PUT request handling"""
        self.client.login(username='testuser', password='testpass')
        
        update_data = {
            'title': 'Updated Post',
            'content': 'Updated content'
        }
        
        response = self.client.put(f'/api/posts/{self.post.pk}/',
                                 json.dumps(update_data),
                                 content_type='application/json')
        
        self.assertEqual(response.status_code, 200)
        
        # Verify update
        self.post.refresh_from_db()
        self.assertEqual(self.post.title, 'Updated Post')
    
    def test_patch_request(self):
        """Test PATCH request handling"""
        self.client.login(username='testuser', password='testpass')
        
        patch_data = {'title': 'Patched Title'}
        
        response = self.client.patch(f'/api/posts/{self.post.pk}/',
                                   json.dumps(patch_data),
                                   content_type='application/json')
        
        self.assertEqual(response.status_code, 200)
        
        # Verify partial update
        self.post.refresh_from_db()
        self.assertEqual(self.post.title, 'Patched Title')
        self.assertEqual(self.post.content, 'Test content')  # Unchanged
    
    def test_delete_request(self):
        """Test DELETE request handling"""
        self.client.login(username='testuser', password='testpass')
        
        response = self.client.delete(f'/api/posts/{self.post.pk}/')
        
        self.assertEqual(response.status_code, 204)
        
        # Verify deletion
        self.assertFalse(Post.objects.filter(pk=self.post.pk).exists())
    
    def test_options_request(self):
        """Test OPTIONS request handling"""
        response = self.client.options(f'/api/posts/{self.post.pk}/')
        
        self.assertEqual(response.status_code, 200)
        self.assertIn('Allow', response)
        self.assertIn('GET', response['Allow'])
    
    def test_method_not_allowed(self):
        """Test method not allowed handling"""
        # Try TRACE method (not typically allowed)
        response = self.client.trace('/api/posts/')
        
        self.assertEqual(response.status_code, 405)
    
    def test_content_type_handling(self):
        """Test different content types"""
        self.client.login(username='testuser', password='testpass')
        
        # JSON content type
        json_data = {'title': 'JSON Post', 'content': 'JSON content'}
        response = self.client.post('/api/posts/',
                                  json.dumps(json_data),
                                  content_type='application/json')
        
        self.assertEqual(response.status_code, 201)
        
        # Form content type
        form_data = {'title': 'Form Post', 'content': 'Form content'}
        response = self.client.post('/api/posts/', form_data)
        
        self.assertEqual(response.status_code, 201)

Proper HTTP method handling is essential for building RESTful APIs and web applications that follow HTTP standards. Understanding when and how to use different methods enables you to create intuitive, predictable interfaces that work well with various clients and tools.