Backend Implementation Documentation - bounswe/bounswe2025group8 GitHub Wiki
This document provides guidelines and standards for implementing the backend API layer of our project. The team has already created database models and tested them in Docker. Now we need to build a robust REST API layer on top of these models to expose our data to clients.
At present, our project includes:
-
models/directory - Contains database class definitions with properly defined relationships, fields, and methods -
tests/directory - Contains tests that validate the functionality of our models
The next phase of development requires implementing these additional components:
project_root/
├── apps/
│ ├── core/
│ │ ├── serializers.py # Data transformation and validation
│ │ ├── views.py # Request handling and business logic
│ │ ├── urls.py # URL routing configuration
│ │ ├── permissions.py # Access control definitions
Purpose: Serializers are the bridge between Django models and the JSON responses our API will return. They handle:
- Converting model instances to JSON for API responses
- Converting incoming JSON data to model instances for database operations
- Validating incoming data based on defined rules
- Handling nested relationships between models
Each model will need a corresponding serializer class:
# apps/core/serializers.py
from rest_framework import serializers
from models.product import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ['id', 'name', 'price', 'description', 'category']Best Practices:
- Create separate serializers for different use cases (e.g., list vs. detail views)
- Implement custom validation when model-level validation isn't sufficient
- Handle nested relationships appropriately (consider performance implications)
Purpose: Views handle HTTP requests and apply business logic. They:
- Process incoming requests and route them to the appropriate handler methods
- Apply authentication and permission checks
- Execute business logic
- Return appropriate responses with status codes
Django REST Framework offers several view types including APIView, generic views, and ViewSets:
# apps/core/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from models.product import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsAuthenticated]Best Practices:
- Use ViewSets for standard CRUD operations
- Use APIView for custom, complex endpoints
- Keep business logic in services or managers, not in views
- Always validate input data using serializers
Purpose: URL configurations map URL patterns to view functions, creating the routing system for your API. They:
- Define the URL structure of your API
- Connect URLs to the appropriate views
- Enable DRF features like automatic URL generation for ViewSets
The Django REST Framework router system makes URL configuration simpler:
# apps/core/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet
router = DefaultRouter()
router.register(r'products', ProductViewSet)
urlpatterns = [
path('', include(router.urls)),
]
# config/urls.py
from django.urls import path, include
urlpatterns = [
path('api/v1/', include('apps.core.urls')),
]Best Practices:
- Use versioning in your API URLs (e.g.,
/api/v1/) - Maintain a logical, resource-oriented URL structure
- Use proper HTTP methods (GET, POST, PUT, DELETE) rather than encoding actions in URLs
- Consider adding documentation endpoints (e.g., Swagger/OpenAPI)
JSON Web Tokens (JWT) provide a modern, stateless approach to authentication that's well-suited for RESTful APIs. Unlike session-based authentication, JWT doesn't require storing session data on the server, making it more scalable and suitable for distributed systems.
Key Benefits of JWT:
- Stateless: No server-side session storage required
- Portable: The same token works across different services
- Secure: Tokens can be signed to verify authenticity
- Efficient: Reduces database lookups for authentication
- User Login: User provides credentials (username/password)
- Token Generation: Server validates credentials and issues a JWT
- Token Storage: Client stores the token (typically in local storage)
- Authenticated Requests: Client includes the token in the Authorization header of subsequent requests
- Token Verification: Server verifies the token's signature and extracts user information
-
Install required package:
pip install djangorestframework-simplejwt -
Configure in settings.py:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), }
-
Add token endpoints:
# config/urls.py from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns = [ path('api/token/', TokenObtainPairView.as_view()), path('api/token/refresh/', TokenRefreshView.as_view()), ]
- Set appropriate token lifetimes (short for access tokens, longer for refresh tokens)
- Use HTTPS to prevent token interception
- Consider implementing token blacklisting for logout functionality
- Be mindful of where tokens are stored on the client side (localStorage vs. HttpOnly cookies)
Our API should follow RESTful design principles:
- Resource-Oriented: Structure the API around resources (nouns, not verbs)
-
Standard HTTP Methods: Use appropriate HTTP methods for operations:
- GET: Retrieve resources
- POST: Create resources
- PUT/PATCH: Update resources
- DELETE: Remove resources
- Statelessness: Each request should contain all information needed to process it
- Proper Status Codes: Use appropriate HTTP status codes (200 OK, 201 Created, 400 Bad Request, etc.)
Standardizing API responses ensures consistency and makes client-side development easier. All endpoints should follow this structure:
# Success response
{
"status": "success",
"data": { ... }
}
# Error response
{
"status": "error",
"message": "Error message",
"errors": { ... }
}This format provides clear status indication and separates metadata from actual response data.
Comprehensive error handling improves API usability and debugging:
- Use Descriptive Messages: Error messages should be clear and helpful
- Provide Field-Level Errors: For validation errors, specify which fields failed and why
- Include Error Codes: Consider adding error codes for programmatic handling by clients
- Log Detailed Information: Log detailed error information server-side while providing appropriate client-facing messages
Implementation example:
# apps/core/utils.py
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
response.data = {
'status': 'error',
'message': 'An error occurred',
'errors': response.data
}
return responseFor collections of resources:
- Implement pagination for large datasets (limit/offset or cursor-based)
- Allow filtering by resource attributes
- Enable sorting by relevant fields
- Consider supporting field selection to reduce payload size
A robust testing strategy ensures API reliability and makes future changes safer. Our approach should include:
- Test individual components in isolation
- Focus on model methods, serializer validation, and utility functions
- Use Django's TestCase for database-related tests
- Test complete API endpoints
- Verify correct behavior of the entire request/response cycle
- Ensure proper integration between components (serializers, views, models)
- Use DRF's APITestCase for testing API endpoints
# Example API test
from rest_framework.test import APITestCase
from django.urls import reverse
from models.user import User
class ProductAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='test', password='test123')
self.client.force_authenticate(user=self.user)
def test_get_products(self):
url = reverse('product-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)- Use Postman or similar tools for interactive API testing
- Create a Postman collection that covers all endpoints
- Set up environment variables for different testing scenarios
- Document example requests and expected responses
- Write tests from the beginning, not as an afterthought
- Aim for high test coverage, especially for critical functionality
- Use fixtures or factories to generate test data
- Test edge cases and error conditions, not just the happy path
- Keep tests independent and idempotent (can be run in any order)
- Create the basic directory structure
- Set up JWT authentication infrastructure
- Implement custom exception handling
- Implement serializers for existing models
- Start with simpler models and work toward more complex ones
- Handle relationships between models appropriately
- Create API views for each resource
- Implement proper permissions and authentication
- Add filtering, pagination, and sorting capabilities
- Configure URL routing with versioning
- Write comprehensive tests for all endpoints
- Set up Postman collection for manual testing
- Document the API (consider adding Swagger/OpenAPI)
- Perform security and performance reviews
- Configure Docker for production deployment
- Set up CI/CD pipeline for automated testing
- Implement monitoring and logging