# 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

```python
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

```python
# 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:

```python
# 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

- [Django REST Framework](https://www.django-rest-framework.org/)
- [Content Negotiation](https://www.django-rest-framework.org/api-guide/content-negotiation/)
- [ThrillWiki API Documentation](../THRILLWIKI_API_DOCUMENTATION.md)
