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-yasgConfiguration
# 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 schemaSwagger UI Features
Try it out:
1. Click "Try it out" button2. Fill in parameters3. Click "Execute"4. See responseAuthentication:
1. Click "Authorize" button2. Enter token/credentials3. Subsequent requests include authReDoc
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:
- ← Bài 10.1: API Versioning
- Bài 10.2: OpenAPI & Swagger (current)
- Bài 10.3: drf-spectacular →