mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 15:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
4.5 KiB
4.5 KiB
ADR-002: Hybrid API Design Pattern
Status
Accepted
Context
ThrillWiki serves two types of clients:
- Web browsers: Need HTML responses for rendering pages
- API clients: Need JSON responses for mobile apps and integrations
We needed to decide how to handle these different client types efficiently without duplicating business logic.
Decision
We implemented a Hybrid API Design Pattern where views can serve both HTML and JSON responses based on content negotiation.
Implementation Strategy
- API-first approach: All business logic is implemented in serializers and services
- Content negotiation: Views check request headers to determine response format
- Dual endpoints: Some resources are available at both web and API URLs
- Shared serializers: Same serializers used for both API responses and template context
URL Structure
# Web URLs (HTML responses)
/parks/ # Park list page
/parks/cedar-point/ # Park detail page
# API URLs (JSON responses)
/api/v1/parks/ # Park list (JSON)
/api/v1/parks/cedar-point/ # Park detail (JSON)
View Implementation
class HybridViewMixin:
"""
Mixin that enables views to serve both HTML and JSON responses.
"""
serializer_class = None
def get_response_format(self, request):
"""Determine response format from Accept header or query param."""
if request.htmx:
return 'html'
if 'application/json' in request.headers.get('Accept', ''):
return 'json'
if request.GET.get('format') == 'json':
return 'json'
return 'html'
def render_response(self, request, context, **kwargs):
"""Render appropriate response based on format."""
format = self.get_response_format(request)
if format == 'json':
serializer = self.serializer_class(context['object'])
return JsonResponse(serializer.data)
return super().render_to_response(context, **kwargs)
Serializer Patterns
# API serializers use camelCase for JSON responses
class ParkSerializer(serializers.ModelSerializer):
operatorName = serializers.CharField(source='operator.name')
rideCount = serializers.IntegerField(source='ride_count')
class Meta:
model = Park
fields = ['id', 'name', 'slug', 'operatorName', 'rideCount']
Consequences
Benefits
- Code Reuse: Single set of business logic serves both web and API
- Consistency: API and web views always return consistent data
- Flexibility: Easy to add new response formats
- Progressive Enhancement: API available without duplicating work
- Mobile-Ready: API ready for mobile app development
Trade-offs
- Complexity: Views need to handle multiple response formats
- Testing: Need to test both HTML and JSON responses
- Documentation: Must document both web and API interfaces
Response Format Decision
| Request Type | Response Format |
|---|---|
| HTMX request | HTML partial |
| Browser (Accept: text/html) | Full HTML page |
| API (Accept: application/json) | JSON |
| Query param (?format=json) | JSON |
Alternatives Considered
Separate API and Web Views
Rejected because:
- Duplicate business logic
- Risk of divergence between API and web
- More code to maintain
API-Only with JavaScript Frontend
Rejected because:
- Conflicts with ADR-001 (Django + HTMX architecture)
- Poor SEO without SSR
- Increased complexity
GraphQL
Rejected because:
- Overkill for current requirements
- Steeper learning curve
- Less mature Django ecosystem support
Implementation Details
Hybrid Loader Services
For complex data loading scenarios, we use hybrid loader services:
# backend/apps/parks/services/hybrid_loader.py
class ParkHybridLoader:
"""
Loads park data optimized for both API and web contexts.
"""
def load_park_detail(self, slug, context='web'):
park = Park.objects.optimized_for_detail().get(slug=slug)
if context == 'api':
return ParkSerializer(park).data
return {'park': park}
API Versioning
API endpoints are versioned to allow breaking changes:
/api/v1/parks/ # Current version
/api/v2/parks/ # Future version (if needed)