Project: REST API Blog

Introduction

This project exposes the blog domain as a versioned JSON API under /api/v1/ using Django REST framework. Public clients read published posts; authenticated users create and edit their own posts with Token authentication. You validate with curl or Postman and optionally connect a separate frontend—patterns from DRF basics and API auth/CORS applied end to end.

Prerequisites

Project Goals

MethodEndpointAuthAction
GET/api/v1/posts/PublicList published posts (paginated)
GET/api/v1/posts/{id}/PublicPost detail
POST/api/v1/posts/TokenCreate post
PUT/PATCH/api/v1/posts/{id}/Token + authorUpdate own post
DELETE/api/v1/posts/{id}/Token + authorDelete own post
POST/api/v1/auth/token/CredentialsObtain token

Optional: filter by ?category=news, ?search=django.

Step 1: Install and Settings

bash
pip install djangorestframework django-filter django-cors-headers

config/settings/base.py:

bash
python manage.py migrate

Step 2: Serializers

blog/serializers.py:

Use lighter serializer on list, full on detail—common DRF pattern.

Step 3: Permissions

blog/permissions.py:

python
from rest_framework.permissions import BasePermission, SAFE_METHODS
 
 
class IsAuthorOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        return obj.author == request.user

Step 4: ViewSet

blog/viewsets.py:

Step 5: URL Routing

blog/urls_api.py:

config/urls.py:

python
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("blog.urls_api")),
    path("", include("blog.urls")),  # optional MTV site
]

Step 6: Manual API Test

Start server:

bash
python manage.py runserver

List (public):

bash
curl http://127.0.0.1:8000/api/v1/posts/

Obtain token:

bash
curl -X POST http://127.0.0.1:8000/api/v1/auth/token/ \
  -d "username=alice&password=pass12345"

Create post:

bash
curl -X POST http://127.0.0.1:8000/api/v1/posts/ \
  -H "Authorization: Token YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "API Post",
    "slug": "api-post",
    "body": "Created via DRF",
    "excerpt": "Short",
    "published": true
  }'

Update (author only):

bash
curl -X PATCH http://127.0.0.1:8000/api/v1/posts/1/ \
  -H "Authorization: Token YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated title"}'

Search:

bash
curl "http://127.0.0.1:8000/api/v1/posts/?search=django"

Step 7: API Tests

blog/tests/test_api_project.py:

bash
python manage.py test blog.tests.test_api_project

Step 8: Optional Frontend Snippet

Vue/React fetch (dev CORS enabled):

javascript
const API = "http://127.0.0.1:8000/api/v1";
 
export async function fetchPosts(token) {
  const headers = token ? { Authorization: `Token ${token}` } : {};
  const res = await fetch(`${API}/posts/`, { headers });
  return res.json();
}

Production: same-origin Nginx — API auth chapter.

Step 9: OpenAPI (Optional)

bash
pip install drf-spectacular

Add to INSTALLED_APPS and REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] — browse /api/schema/swagger-ui/.

Compare Flask REST project and Spring Boot REST.

Acceptance Checklist

  • Anonymous GET list/detail works for published posts
  • Unauthenticated POST returns 401/403
  • Token login + create/update own post works
  • PATCH on another user’s post returns 403
  • Pagination and search query params work
  • API tests pass in CI — Docker and CI

Common Problems

IssueFix
403 on POST with tokenCheck Authorization: Token ... header spelling
Draft visible in APIFilter published=True in get_queryset for list
CORS error from SPAcorsheaders + correct origin
category_id validationEnsure category exists in DB
Browsable API in prodDisable HTML renderer in REST_FRAMEWORK

FAQ## FAQ

JWT instead of Token?

Replace with simplejwt — better for SPAs with refresh tokens.

API versioning v2?

Add path("api/v2/", include(...)) with new serializers.

Rate limiting?

DRF AnonRateThrottle / UserRateThrottle.

GraphQL?

Separate graphene-django stack—not covered here.

Hide unpublished from authors in list?

Custom get_queryset: public sees published; authors see own drafts via query param ?mine=1.