Files
thrillwiki_django_no_react/docs/architecture/adr-002-hybrid-api-design.md
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- 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.
2025-12-23 16:41:42 -05:00

4.5 KiB

ADR-002: Hybrid API Design Pattern

Status

Accepted

Context

ThrillWiki serves two types of clients:

  1. Web browsers: Need HTML responses for rendering pages
  2. 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

  1. API-first approach: All business logic is implemented in serializers and services
  2. Content negotiation: Views check request headers to determine response format
  3. Dual endpoints: Some resources are available at both web and API URLs
  4. 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

  1. Code Reuse: Single set of business logic serves both web and API
  2. Consistency: API and web views always return consistent data
  3. Flexibility: Easy to add new response formats
  4. Progressive Enhancement: API available without duplicating work
  5. Mobile-Ready: API ready for mobile app development

Trade-offs

  1. Complexity: Views need to handle multiple response formats
  2. Testing: Need to test both HTML and JSON responses
  3. 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)

References