Bài 10.2: API Versioning & Documentation - Phần 2 - OpenAPI & Swagger

**Series Navigation:** - [Bài 10.1: API Versioning](./010-api-versioning-documentation.md) - **Bài 10.2 (bài này): OpenAPI & Swagger** 👈 - [Bài 10.3: drf-spectacular](./010-api-versioning-documentation-part3.md) - [Bài 10.4: Advanced Documentation](./010-api-versioning-documentation-part4.md)

Mục Tiêu Bài 10.2

Sau khi hoàn thành Bài 10.2 này, bạn sẽ:

  • ✅ Understand OpenAPI specification
  • ✅ Generate API schema
  • ✅ Use Swagger UI
  • ✅ Use ReDoc
  • ✅ Customize schema generation
  • ✅ Add schema descriptions

What is OpenAPI?

OpenAPI Specification = Standard format for describing REST APIs.

Previously known as: Swagger Specification

Format: JSON or YAML

Contains:

  • Available endpoints
  • Request/response formats
  • Authentication methods
  • Parameters
  • Error responses

Benefits:

  • 📖 Auto-generate documentation
  • 🧪 Auto-generate test clients
  • 🔧 Auto-generate SDKs
  • ✅ API validation

DRF Schema Generation

Built-in Schema Generator

# urls.pyfrom rest_framework.schemas import get_schema_view schema_view = get_schema_view(    title='My API',    description='API for all things ...',    version='1.0.0') urlpatterns = [    path('openapi/', schema_view, name='openapi-schema'),    # ... your API URLs]

Access schema:

curl http://localhost:8000/openapi/

CoreAPI Schema (Legacy)

# settings.pyREST_FRAMEWORK = {    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',} # urls.pyfrom rest_framework.schemas import get_schema_viewfrom rest_framework.documentation import include_docs_urls urlpatterns = [    path('schema/', get_schema_view(        title='My API',        description='API Description',        version='1.0.0'    )),    path('docs/', include_docs_urls(title='My API')),]

OpenAPI Schema (DRF 3.12+)

# settings.pyREST_FRAMEWORK = {    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',} # urls.pyfrom rest_framework.schemas import get_schema_view schema_view = get_schema_view(    title='My API',    description='Comprehensive API for my application',    version='1.0.0',    public=True,) urlpatterns = [    path('openapi.json', schema_view, name='openapi-schema'),]

Swagger UI

What is Swagger UI?

Swagger UI = Interactive documentation UI for OpenAPI specs.

Features:

  • 📖 Browse API endpoints
  • 🧪 Test endpoints directly
  • 📝 See request/response examples
  • 🔒 Handle authentication

Installation

pip install drf-yasg

Configuration

# settings.pyINSTALLED_APPS = [    # ...    'drf_yasg',] # urls.pyfrom rest_framework import permissionsfrom drf_yasg.views import get_schema_viewfrom drf_yasg import openapi schema_view = get_schema_view(    openapi.Info(        title="My API",        default_version='v1',        description="API documentation",        terms_of_service="https://www.example.com/terms/",        contact=openapi.Contact(email="[email protected]"),        license=openapi.License(name="BSD License"),    ),    public=True,    permission_classes=[permissions.AllowAny],) urlpatterns = [    # Swagger UI    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),        # ReDoc UI    path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),        # JSON schema    path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'),    path('swagger.yaml', schema_view.without_ui(cache_timeout=0), name='schema-yaml'),]

Access documentation:

http://localhost:8000/swagger/   # Swagger UIhttp://localhost:8000/redoc/      # ReDoc UIhttp://localhost:8000/swagger.json  # JSON schema

Swagger UI Features

Try it out:

1. Click "Try it out" button2. Fill in parameters3. Click "Execute"4. See response

Authentication:

1. Click "Authorize" button2. Enter token/credentials3. Subsequent requests include auth

ReDoc

What is ReDoc?

ReDoc = Another documentation UI for OpenAPI.

Differences from Swagger UI:

Feature Swagger UI ReDoc
Interactivity ⭐⭐⭐⭐⭐ ⭐⭐⭐
Design Good Better
Navigation Good Better
Testing Yes No
Use case Development Production docs

Access ReDoc

# Already configured in urls.py abovepath('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),

Visit: http://localhost:8000/redoc/

Customizing Schema

Adding Descriptions to ViewSets

# views.pyfrom rest_framework import viewsetsfrom drf_yasg.utils import swagger_auto_schemafrom drf_yasg import openapi class ArticleViewSet(viewsets.ModelViewSet):    """    API endpoint for articles.        Supports CRUD operations on articles with authentication.    """    queryset = Article.objects.all()    serializer_class = ArticleSerializer        @swagger_auto_schema(        operation_description="List all articles",        responses={200: ArticleSerializer(many=True)}    )    def list(self, request, *args, **kwargs):        """Get list of all articles"""        return super().list(request, *args, **kwargs)        @swagger_auto_schema(        operation_description="Retrieve a specific article",        responses={            200: ArticleSerializer(),            404: 'Article not found'        }    )    def retrieve(self, request, *args, **kwargs):        """Get article details by ID"""        return super().retrieve(request, *args, **kwargs)

Adding Query Parameters

class ArticleViewSet(viewsets.ModelViewSet):    queryset = Article.objects.all()    serializer_class = ArticleSerializer        @swagger_auto_schema(        operation_description="List articles with optional filtering",        manual_parameters=[            openapi.Parameter(                'status',                openapi.IN_QUERY,                description="Filter by status",                type=openapi.TYPE_STRING,                enum=['draft', 'published', 'archived']            ),            openapi.Parameter(                'author',                openapi.IN_QUERY,                description="Filter by author ID",                type=openapi.TYPE_INTEGER            ),            openapi.Parameter(                'search',                openapi.IN_QUERY,                description="Search in title and content",                type=openapi.TYPE_STRING            ),        ],        responses={200: ArticleSerializer(many=True)}    )    def list(self, request, *args, **kwargs):        return super().list(request, *args, **kwargs)

Adding Request Body Schema

class ArticleViewSet(viewsets.ModelViewSet):    queryset = Article.objects.all()    serializer_class = ArticleSerializer        @swagger_auto_schema(        operation_description="Create a new article",        request_body=ArticleSerializer,        responses={            201: ArticleSerializer(),            400: 'Invalid input'        }    )    def create(self, request, *args, **kwargs):        """Create new article"""        return super().create(request, *args, **kwargs)        @swagger_auto_schema(        operation_description="Update an article",        request_body=ArticleSerializer,        responses={            200: ArticleSerializer(),            400: 'Invalid input',            404: 'Article not found'        }    )    def update(self, request, *args, **kwargs):        """Update article completely"""        return super().update(request, *args, **kwargs)

Custom Actions Documentation

class ArticleViewSet(viewsets.ModelViewSet):    queryset = Article.objects.all()    serializer_class = ArticleSerializer        @swagger_auto_schema(        method='post',        operation_description="Publish a draft article",        responses={            200: ArticleSerializer(),            400: 'Article is already published',            404: 'Article not found'        }    )    @action(detail=True, methods=['post'])    def publish(self, request, pk=None):        """Publish article"""        article = self.get_object()                if article.status == 'published':            return Response(                {'error': 'Article is already published'},                status=400            )                article.status = 'published'        article.save()                serializer = self.get_serializer(article)        return Response(serializer.data)        @swagger_auto_schema(        method='post',        operation_description="Like an article",        responses={            200: openapi.Response(                'Article liked',                schema=openapi.Schema(                    type=openapi.TYPE_OBJECT,                    properties={                        'likes': openapi.Schema(type=openapi.TYPE_INTEGER),                        'message': openapi.Schema(type=openapi.TYPE_STRING)                    }                )            )        }    )    @action(detail=True, methods=['post'])    def like(self, request, pk=None):        """Like article"""        article = self.get_object()        article.likes += 1        article.save()                return Response({            'likes': article.likes,            'message': 'Article liked successfully'        })

Response Examples

@swagger_auto_schema(    operation_description="Get article with example response",    responses={        200: openapi.Response(            description="Article details",            schema=ArticleSerializer(),            examples={                'application/json': {                    'id': 1,                    'title': 'Sample Article',                    'content': 'This is sample content',                    'author': {                        'id': 5,                        'username': 'john'                    },                    'status': 'published',                    'views': 100,                    'likes': 10,                    'created_at': '2024-01-01T00:00:00Z'                }            }        ),        404: 'Article not found'    })def retrieve(self, request, *args, **kwargs):    return super().retrieve(request, *args, **kwargs)

Schema Tags

Organizing Endpoints with Tags

# settings.py (drf-yasg)SWAGGER_SETTINGS = {    'USE_SESSION_AUTH': False,    'SECURITY_DEFINITIONS': {        'Bearer': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header'        }    },    'TAGS_SORTER': 'alpha',} # views.pyfrom drf_yasg.utils import swagger_auto_schema class ArticleViewSet(viewsets.ModelViewSet):    """    Articles API    """    queryset = Article.objects.all()    serializer_class = ArticleSerializer        @swagger_auto_schema(tags=['Articles'])    def list(self, request, *args, **kwargs):        return super().list(request, *args, **kwargs)        @swagger_auto_schema(tags=['Articles'])    def create(self, request, *args, **kwargs):        return super().create(request, *args, **kwargs) class CommentViewSet(viewsets.ModelViewSet):    """    Comments API    """    queryset = Comment.objects.all()    serializer_class = CommentSerializer        @swagger_auto_schema(tags=['Comments'])    def list(self, request, *args, **kwargs):        return super().list(request, *args, **kwargs)

Authentication in Schema

Token Authentication

# urls.pyschema_view = get_schema_view(    openapi.Info(        title="My API",        default_version='v1',        description="API with token authentication",    ),    public=True,    permission_classes=[permissions.AllowAny],) # settings.pySWAGGER_SETTINGS = {    'SECURITY_DEFINITIONS': {        'Token': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header',            'description': 'Token-based authentication. Format: "Token <your-token>"'        }    },} # views.pyclass ArticleViewSet(viewsets.ModelViewSet):    permission_classes = [IsAuthenticated]        @swagger_auto_schema(        security=[{'Token': []}],  # Requires token auth        responses={            200: ArticleSerializer(many=True),            401: 'Unauthorized'        }    )    def list(self, request, *args, **kwargs):        return super().list(request, *args, **kwargs)

JWT Authentication

# settings.pySWAGGER_SETTINGS = {    'SECURITY_DEFINITIONS': {        'Bearer': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header',            'description': 'JWT Authorization. Format: "Bearer <your-jwt-token>"'        }    },    'USE_SESSION_AUTH': False,}

Multiple Auth Methods

SWAGGER_SETTINGS = {    'SECURITY_DEFINITIONS': {        'Token': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header'        },        'Bearer': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header'        },        'Basic': {            'type': 'basic'        }    },}

Serializer Documentation

Field Descriptions

# serializers.pyclass ArticleSerializer(serializers.ModelSerializer):    title = serializers.CharField(        max_length=200,        help_text="Article title (max 200 characters)"    )    content = serializers.CharField(        help_text="Article content in Markdown format"    )    status = serializers.ChoiceField(        choices=['draft', 'published', 'archived'],        default='draft',        help_text="Publication status"    )    author = serializers.SerializerMethodField(        help_text="Author information"    )        class Meta:        model = Article        fields = ['id', 'title', 'content', 'status', 'author', 'created_at']        read_only_fields = ['id', 'created_at']        def get_author(self, obj):        """Get author details"""        return {            'id': obj.author.id,            'username': obj.author.username        }

Example Values

class ArticleSerializer(serializers.ModelSerializer):    title = serializers.CharField(        max_length=200,        help_text="Article title",        style={'placeholder': 'Enter article title'}    )        class Meta:        model = Article        fields = ['id', 'title', 'content', 'status']                # Swagger examples        swagger_schema_fields = {            'example': {                'id': 1,                'title': 'Sample Article',                'content': 'This is sample content',                'status': 'published'            }        }

Complete Documentation Example

# models.pyfrom django.db import modelsfrom django.contrib.auth.models import User class Article(models.Model):    STATUS_CHOICES = [        ('draft', 'Draft'),        ('published', 'Published'),        ('archived', 'Archived'),    ]        title = models.CharField(max_length=200)    content = models.TextField()    author = models.ForeignKey(User, on_delete=models.CASCADE)    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')    views = models.IntegerField(default=0)    likes = models.IntegerField(default=0)    created_at = models.DateTimeField(auto_now_add=True)    updated_at = models.DateTimeField(auto_now=True) # serializers.pyclass ArticleSerializer(serializers.ModelSerializer):    """    Article serializer with full documentation    """    author = serializers.SerializerMethodField(        help_text="Author information including ID and username"    )        class Meta:        model = Article        fields = [            'id', 'title', 'content', 'author', 'status',            'views', 'likes', 'created_at', 'updated_at'        ]        read_only_fields = ['id', 'views', 'likes', 'created_at', 'updated_at']                swagger_schema_fields = {            'example': {                'id': 1,                'title': 'Getting Started with Django',                'content': 'Django is a high-level Python web framework...',                'author': {'id': 5, 'username': 'john'},                'status': 'published',                'views': 1500,                'likes': 42,                'created_at': '2024-01-01T00:00:00Z',                'updated_at': '2024-01-02T00:00:00Z'            }        }        def get_author(self, obj):        return {            'id': obj.author.id,            'username': obj.author.username        } # views.pyfrom drf_yasg.utils import swagger_auto_schemafrom drf_yasg import openapi class ArticleViewSet(viewsets.ModelViewSet):    """    API endpoint for managing articles.        Provides CRUD operations for articles with authentication required.    Articles can be filtered, searched, and paginated.    """    queryset = Article.objects.select_related('author').all()    serializer_class = ArticleSerializer    permission_classes = [IsAuthenticatedOrReadOnly]    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]    filterset_fields = ['status', 'author']    search_fields = ['title', 'content']    ordering_fields = ['created_at', 'views', 'likes']        @swagger_auto_schema(        operation_summary="List articles",        operation_description="Get a paginated list of articles with optional filtering and search",        manual_parameters=[            openapi.Parameter(                'status',                openapi.IN_QUERY,                description="Filter by publication status",                type=openapi.TYPE_STRING,                enum=['draft', 'published', 'archived']            ),            openapi.Parameter(                'author',                openapi.IN_QUERY,                description="Filter by author ID",                type=openapi.TYPE_INTEGER            ),            openapi.Parameter(                'search',                openapi.IN_QUERY,                description="Search in title and content",                type=openapi.TYPE_STRING            ),            openapi.Parameter(                'ordering',                openapi.IN_QUERY,                description="Order by field (prefix with - for descending)",                type=openapi.TYPE_STRING,                enum=['created_at', '-created_at', 'views', '-views', 'likes', '-likes']            ),        ],        responses={            200: ArticleSerializer(many=True),            401: 'Unauthorized'        },        tags=['Articles']    )    def list(self, request, *args, **kwargs):        """Get list of articles"""        return super().list(request, *args, **kwargs)        @swagger_auto_schema(        operation_summary="Create article",        operation_description="Create a new article (authentication required)",        request_body=ArticleSerializer,        responses={            201: ArticleSerializer(),            400: 'Invalid input',            401: 'Unauthorized'        },        security=[{'Bearer': []}],        tags=['Articles']    )    def create(self, request, *args, **kwargs):        """Create new article"""        return super().create(request, *args, **kwargs)        @swagger_auto_schema(        operation_summary="Get article",        operation_description="Retrieve a specific article by ID",        responses={            200: ArticleSerializer(),            404: 'Article not found'        },        tags=['Articles']    )    def retrieve(self, request, *args, **kwargs):        """Get article details"""        return super().retrieve(request, *args, **kwargs)        @swagger_auto_schema(        method='post',        operation_summary="Publish article",        operation_description="Change article status to published",        responses={            200: openapi.Response(                'Article published successfully',                ArticleSerializer()            ),            400: 'Article is already published',            401: 'Unauthorized',            404: 'Article not found'        },        security=[{'Bearer': []}],        tags=['Articles', 'Actions']    )    @action(detail=True, methods=['post'])    def publish(self, request, pk=None):        """Publish article"""        article = self.get_object()        article.status = 'published'        article.save()        serializer = self.get_serializer(article)        return Response(serializer.data) # urls.pyfrom django.urls import path, includefrom rest_framework.routers import DefaultRouterfrom rest_framework import permissionsfrom drf_yasg.views import get_schema_viewfrom drf_yasg import openapi # API routerrouter = DefaultRouter()router.register('articles', ArticleViewSet, basename='article') # Schema viewschema_view = get_schema_view(    openapi.Info(        title="Articles API",        default_version='v1',        description="""        # Articles API Documentation                This API provides endpoints for managing articles with the following features:                ## Features        - CRUD operations on articles        - Filtering by status and author        - Full-text search        - Pagination        - JWT authentication                ## Authentication        Most endpoints require JWT authentication. Include your token in the Authorization header:        ```        Authorization: Bearer <your-jwt-token>        ```                ## Rate Limiting        - Authenticated users: 1000 requests/hour        - Anonymous users: 100 requests/hour        """,        terms_of_service="https://www.example.com/terms/",        contact=openapi.Contact(email="[email protected]"),        license=openapi.License(name="MIT License"),    ),    public=True,    permission_classes=[permissions.AllowAny],) urlpatterns = [    # API endpoints    path('api/', include(router.urls)),        # Documentation    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),    path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),        # Schema download    path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'),    path('swagger.yaml', schema_view.without_ui(cache_timeout=0), name='schema-yaml'),] # settings.pySWAGGER_SETTINGS = {    'SECURITY_DEFINITIONS': {        'Bearer': {            'type': 'apiKey',            'name': 'Authorization',            'in': 'header',            'description': 'JWT token authentication. Format: Bearer <token>'        }    },    'USE_SESSION_AUTH': False,    'PERSIST_AUTH': True,}

Tóm Tắt Bài 10.2

Trong Bài 10.2 này, bạn đã học:

OpenAPI Specification:

  • What is OpenAPI
  • Benefits of OpenAPI
  • Schema format

DRF Schema Generation:

  • Built-in schema generator
  • CoreAPI schema (legacy)
  • OpenAPI schema

Swagger UI:

  • Installation and configuration
  • Interactive documentation
  • Testing endpoints
  • Authentication

ReDoc:

  • Alternative documentation UI
  • Better design
  • Production-ready docs

Customizing Schema:

  • ViewSet descriptions
  • Query parameters
  • Request body schema
  • Custom actions
  • Response examples

Advanced Features:

  • Schema tags
  • Authentication docs
  • Serializer documentation
  • Complete examples

Next Steps

Trong Bài 10.3, bạn sẽ học:

  • drf-spectacular overview
  • Better OpenAPI 3.0 support
  • Advanced schema customization
  • Versioned documentation
  • Production deployment

Navigation: