Traditionelle REST-API mit Django

JSON-APIs ohne Django REST Framework

Von Django Views zu JSON-Responses

Modul 1: Django Basics - Manuelle API

📋 Was lernen wir?

1

JsonResponse

Django's JSON-Antworten

Python Dict → JSON

2

Views erstellen

Function-Based Views

GET, POST, PUT, DELETE

3

URLs konfigurieren

URL-Patterns definieren

API-Endpunkte erstellen

4

CRUD-Operationen

Complete API implementieren

Error Handling

🤔 Warum traditionelle API ohne DRF?

✅ Vorteile

  • Lernen: Verstehen wie APIs funktionieren
  • Kontrolle: Volle Kontrolle über jeden Schritt
  • Einfach: Keine zusätzliche Library
  • Leichtgewichtig: Minimale Dependencies
  • Basis: Fundament für DRF-Verständnis

⚠️ Nachteile

  • Mehr Code schreiben
  • Manuelles Serialisieren
  • Kein automatisches Browsable API
  • Keine Auto-Dokumentation
  • Wiederholender Code

🎯 Ziel dieser Lektion:

Verstehen, was Django REST Framework für uns automatisiert!

📦 JsonResponse - Django's JSON-Antwort

Was ist JsonResponse?

Eine spezielle HttpResponse, die Python-Dicts automatisch in JSON konvertiert

📝

Einfaches Beispiel

from django.http import JsonResponse

def hello_api(request):
    data = {
        'message': 'Hello, API!',
        'status': 'success'
    }
    return JsonResponse(data)

# Response:
# {
#   "message": "Hello, API!",
#   "status": "success"
# }
🔧

JsonResponse Parameter

# Safe=False für Listen
JsonResponse([1, 2, 3], safe=False)

# Status-Code setzen
JsonResponse(data, status=201)

# Custom Headers
response = JsonResponse(data)
response['X-Custom-Header'] = 'value'

# JSON Encoder
JsonResponse(
    data,
    json_dumps_params={'indent': 2}
)

📁 Projekt-Struktur vorbereiten

Unsere Struktur:

FirstMovieAPI/
│
├── movies/
│   ├── models.py          # ✅ Models bereits vorhanden
│   ├── views.py           # ← Hier erstellen wir API-Views
│   ├── urls.py            # ← Neu: API URLs
│   └── utils.py           # ← Neu: Helper-Funktionen
│
├── firstmovieapi/
│   ├── settings.py
│   └── urls.py            # ← Hier einbinden
│
└── manage.py
1

utils.py erstellen

Helper-Funktionen für Serialisierung

2

views.py erweitern

API-Views für CRUD-Operationen

3

urls.py erstellen

API-Endpunkte definieren

🛠️ Schritt 1: Helper-Funktionen (utils.py)

Datei: movies/utils.py (Neu erstellen)

Model → Dictionary Konverter:

"""
Helper-Funktionen für die manuelle API
"""

def movie_to_dict(movie):
    """Konvertiert Movie-Objekt zu Dictionary"""
    return {
        'id': movie.id,
        'title': movie.title,
        'year': movie.year,
        'genre': movie.genre,
        'rating': float(movie.rating) if movie.rating else None,
        'description': movie.description,
        'created_at': movie.created_at.isoformat(),
        'updated_at': movie.updated_at.isoformat(),
    }


def artist_to_dict(artist):
    """Konvertiert Artist-Objekt zu Dictionary"""
    return {
        'id': artist.id,
        'first_name': artist.first_name,
        'last_name': artist.last_name,
        'full_name': artist.full_name,
        'birth_date': artist.birth_date.isoformat() if artist.birth_date else None,
        'nationality': artist.nationality,
        'biography': artist.biography,
        'created_at': artist.created_at.isoformat(),
        'updated_at': artist.updated_at.isoformat(),
    }


def casting_to_dict(casting):
    """Konvertiert MovieCasting-Objekt zu Dictionary"""
    return {
        'id': casting.id,
        'movie': movie_to_dict(casting.movie),
        'artist': artist_to_dict(casting.artist),
        'role_name': casting.role_name,
        'is_main_role': casting.is_main_role,
        'order': casting.order,
        'created_at': casting.created_at.isoformat(),
    }

🔄 Warum Serialisierung?

Problem: Django Models sind keine JSON-Objekte!

❌ Das geht NICHT

from django.http import JsonResponse
from .models import Movie

def movie_list(request):
    movies = Movie.objects.all()
    # ❌ TypeError!
    return JsonResponse(movies)

# Error:
# TypeError: Object of type QuerySet 
# is not JSON serializable

✅ Das geht!

from django.http import JsonResponse
from .models import Movie
from .utils import movie_to_dict

def movie_list(request):
    movies = Movie.objects.all()
    
    # Manuell konvertieren
    data = [movie_to_dict(m) for m in movies]
    
    return JsonResponse(data, safe=False)

# Response: ✅ JSON Array!

💡 Was macht unsere Funktion?

  • Django Model → Python Dictionary
  • DateField → ISO-Format String
  • DecimalField → Float
  • Related Objects → Nested Dicts

📋 Schritt 2: Movie List API (GET)

Datei: movies/views.py

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from .models import Movie
from .utils import movie_to_dict


@require_http_methods(["GET"])
def movie_list(request):
    """
    GET /api/movies/ - Liste aller Filme
    """
    # Alle Filme aus DB holen
    movies = Movie.objects.all()
    
    # In Dictionaries konvertieren
    data = [movie_to_dict(movie) for movie in movies]
    
    # Als JSON zurückgeben
    return JsonResponse(data, safe=False)


# Response Beispiel:
# [
#   {
#     "id": 1,
#     "title": "The Matrix",
#     "year": 1999,
#     "genre": "Sci-Fi",
#     "rating": 8.7,
#     "description": "...",
#     "created_at": "2024-11-09T10:30:00Z",
#     "updated_at": "2024-11-09T10:30:00Z"
#   },
#   {
#     "id": 2,
#     "title": "Inception",
#     ...
#   }
# ]

🔍 Schritt 3: Movie Detail API (GET)

from django.http import JsonResponse, Http404
from django.views.decorators.http import require_http_methods
from .models import Movie
from .utils import movie_to_dict


@require_http_methods(["GET"])
def movie_detail(request, pk):
    """
    GET /api/movies// - Ein bestimmter Film
    """
    try:
        # Film mit ID holen
        movie = Movie.objects.get(pk=pk)
    except Movie.DoesNotExist:
        # 404 wenn nicht gefunden
        return JsonResponse(
            {'error': 'Movie not found'},
            status=404
        )
    
    # In Dictionary konvertieren
    data = movie_to_dict(movie)
    
    # Als JSON zurückgeben
    return JsonResponse(data)


# Request:
# GET /api/movies/1/

# Response (200 OK):
# {
#   "id": 1,
#   "title": "The Matrix",
#   "year": 1999,
#   "genre": "Sci-Fi",
#   "rating": 8.7,
#   ...
# }

# Request:
# GET /api/movies/999/

# Response (404 Not Found):
# {
#   "error": "Movie not found"
# }

📥 JSON Request-Body parsen

Problem: POST/PUT senden JSON im Body

Django parst das nicht automatisch!

Eigene Helper-Funktion in utils.py:

import json
from django.http import JsonResponse


def parse_json_body(request):
    """
    Parst JSON aus request.body
    Gibt (data, error_response) zurück
    """
    try:
        # Body ist Bytes → String → JSON
        body = request.body.decode('utf-8')
        data = json.loads(body)
        return data, None
    except json.JSONDecodeError:
        # Ungültiges JSON
        error = JsonResponse(
            {'error': 'Invalid JSON'},
            status=400
        )
        return None, error
    except Exception as e:
        # Anderer Fehler
        error = JsonResponse(
            {'error': str(e)},
            status=400
        )
        return None, error

➕ Schritt 4: Movie Create API (POST)

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import Movie
from .utils import movie_to_dict, parse_json_body


@csrf_exempt  # ⚠️ Nur für Entwicklung! Später CSRF-Token nutzen
@require_http_methods(["POST"])
def movie_create(request):
    """
    POST /api/movies/ - Neuen Film erstellen
    
    Body:
    {
        "title": "New Movie",
        "year": 2024,
        "genre": "Action",
        "rating": 8.5,
        "description": "..."
    }
    """
    # JSON parsen
    data, error = parse_json_body(request)
    if error:
        return error
    
    # Validierung
    required_fields = ['title', 'year']
    for field in required_fields:
        if field not in data:
            return JsonResponse(
                {'error': f'{field} is required'},
                status=400
            )
    
    try:
        # Movie erstellen
        movie = Movie.objects.create(
            title=data['title'],
            year=data['year'],
            genre=data.get('genre', ''),
            rating=data.get('rating'),
            description=data.get('description', '')
        )
        
        # Als JSON zurückgeben (201 Created)
        return JsonResponse(
            movie_to_dict(movie),
            status=201
        )
    
    except Exception as e:
        return JsonResponse(
            {'error': str(e)},
            status=400
        )

🔒 CSRF-Protection verstehen

Was ist CSRF?

Cross-Site Request Forgery - Schutz vor böswilligen Requests

⚠️ Development (@csrf_exempt)

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def movie_create(request):
    # Kein CSRF-Check
    # NUR für Entwicklung!
    # NICHT in Production!

Deaktiviert CSRF-Schutz komplett

✅ Production (CSRF-Token)

# Im Frontend: Token holen
const token = getCookie('csrftoken');

fetch('/api/movies/', {
    method: 'POST',
    headers: {
        'X-CSRFToken': token,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
})

# Django prüft Token automatisch

Sicher für Production!

🎯 Für dieses Tutorial:

Wir nutzen @csrf_exempt für Einfachheit. In echten Apps: CSRF-Token nutzen!

✏️ Schritt 5: Movie Update API (PUT)

from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods


@csrf_exempt
@require_http_methods(["PUT"])
def movie_update(request, pk):
    """
    PUT /api/movies// - Film aktualisieren
    
    Body:
    {
        "title": "Updated Title",
        "year": 2024,
        "genre": "Drama",
        "rating": 9.0,
        "description": "Updated..."
    }
    """
    # Film holen
    try:
        movie = Movie.objects.get(pk=pk)
    except Movie.DoesNotExist:
        return JsonResponse(
            {'error': 'Movie not found'},
            status=404
        )
    
    # JSON parsen
    data, error = parse_json_body(request)
    if error:
        return error
    
    # Felder aktualisieren
    movie.title = data.get('title', movie.title)
    movie.year = data.get('year', movie.year)
    movie.genre = data.get('genre', movie.genre)
    movie.rating = data.get('rating', movie.rating)
    movie.description = data.get('description', movie.description)
    
    try:
        movie.save()
        return JsonResponse(movie_to_dict(movie))
    except Exception as e:
        return JsonResponse(
            {'error': str(e)},
            status=400
        )

🗑️ Schritt 6: Movie Delete API (DELETE)

from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods


@csrf_exempt
@require_http_methods(["DELETE"])
def movie_delete(request, pk):
    """
    DELETE /api/movies// - Film löschen
    """
    # Film holen
    try:
        movie = Movie.objects.get(pk=pk)
    except Movie.DoesNotExist:
        return JsonResponse(
            {'error': 'Movie not found'},
            status=404
        )
    
    # Löschen
    movie.delete()
    
    # 204 No Content (kein Body!)
    return JsonResponse({}, status=204)


# Request:
# DELETE /api/movies/1/

# Response:
# Status: 204 No Content
# Body: (leer)

# Nochmal:
# DELETE /api/movies/1/

# Response:
# Status: 404 Not Found
# {
#   "error": "Movie not found"
# }

🔗 Schritt 7: URLs konfigurieren

Datei: movies/urls.py (Neu erstellen)

from django.urls import path
from . import views

urlpatterns = [
    # Movie API Endpoints
    path('movies/', views.movie_list, name='movie-list'),
    path('movies/create/', views.movie_create, name='movie-create'),
    path('movies//', views.movie_detail, name='movie-detail'),
    path('movies//update/', views.movie_update, name='movie-update'),
    path('movies//delete/', views.movie_delete, name='movie-delete'),
]

# Endpunkte:
# GET    /api/movies/              → Liste aller Filme
# POST   /api/movies/create/       → Neuen Film erstellen
# GET    /api/movies/1/            → Film #1 abrufen
# PUT    /api/movies/1/update/     → Film #1 aktualisieren
# DELETE /api/movies/1/delete/     → Film #1 löschen

🔗 Schritt 8: In Haupt-URLs einbinden

Datei: firstmovieapi/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    
    # API Endpoints
    path('api/', include('movies.urls')),
]

# Alle API-Endpunkte sind jetzt unter /api/ erreichbar:
# GET    /api/movies/
# POST   /api/movies/create/
# GET    /api/movies/1/
# PUT    /api/movies/1/update/
# DELETE /api/movies/1/delete/

API ist fertig!

Jetzt können wir testen

🚀 Schritt 9: Server starten & testen

1

Server starten

python manage.py runserver

Server läuft auf http://127.0.0.1:8000/

2

Im Browser testen

Öffne: http://127.0.0.1:8000/api/movies/

Siehst du JSON? ✅

3

Mit curl testen

# Liste abrufen
curl http://127.0.0.1:8000/api/movies/

# Einen Film abrufen
curl http://127.0.0.1:8000/api/movies/1/

# Neuen Film erstellen
curl -X POST http://127.0.0.1:8000/api/movies/create/ \
  -H "Content-Type: application/json" \
  -d '{"title":"New Movie","year":2024}'

# Film aktualisieren
curl -X PUT http://127.0.0.1:8000/api/movies/1/update/ \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Title","year":2024}'

# Film löschen
curl -X DELETE http://127.0.0.1:8000/api/movies/1/delete/

📬 Mit Postman/Thunder Client testen

📋

GET - Liste

GET http://127.0.0.1:8000/api/movies/

Headers: (keine nötig)

Response: 200 OK
[
  {
    "id": 1,
    "title": "The Matrix",
    ...
  }
]

POST - Erstellen

POST http://127.0.0.1:8000/api/movies/create/

Headers:
Content-Type: application/json

Body (raw JSON):
{
  "title": "Interstellar",
  "year": 2014,
  "genre": "Sci-Fi",
  "rating": 8.6,
  "description": "A team of explorers..."
}

Response: 201 Created
{
  "id": 3,
  "title": "Interstellar",
  ...
}
✏️

PUT - Aktualisieren

PUT http://127.0.0.1:8000/api/movies/3/update/

Headers:
Content-Type: application/json

Body (raw JSON):
{
  "title": "Interstellar (IMAX)",
  "year": 2014,
  "rating": 9.0
}

Response: 200 OK
{
  "id": 3,
  "title": "Interstellar (IMAX)",
  ...
}
🗑️

DELETE - Löschen

DELETE http://127.0.0.1:8000/api/movies/3/delete/

Headers: (keine nötig)

Response: 204 No Content
(Kein Body)

📄 Komplette views.py - Übersicht

"""
movies/views.py - Manuelle REST API Views
"""
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import Movie
from .utils import movie_to_dict, parse_json_body


@require_http_methods(["GET"])
def movie_list(request):
    """GET /api/movies/ - Liste aller Filme"""
    movies = Movie.objects.all()
    data = [movie_to_dict(movie) for movie in movies]
    return JsonResponse(data, safe=False)


@require_http_methods(["GET"])
def movie_detail(request, pk):
    """GET /api/movies// - Ein bestimmter Film"""
    try:
        movie = Movie.objects.get(pk=pk)
        return JsonResponse(movie_to_dict(movie))
    except Movie.DoesNotExist:
        return JsonResponse({'error': 'Movie not found'}, status=404)


@csrf_exempt
@require_http_methods(["POST"])
def movie_create(request):
    """POST /api/movies/create/ - Neuen Film erstellen"""
    data, error = parse_json_body(request)
    if error:
        return error
    
    required = ['title', 'year']
    for field in required:
        if field not in data:
            return JsonResponse({'error': f'{field} is required'}, status=400)
    
    try:
        movie = Movie.objects.create(
            title=data['title'],
            year=data['year'],
            genre=data.get('genre', ''),
            rating=data.get('rating'),
            description=data.get('description', '')
        )
        return JsonResponse(movie_to_dict(movie), status=201)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=400)


@csrf_exempt
@require_http_methods(["PUT"])
def movie_update(request, pk):
    """PUT /api/movies//update/ - Film aktualisieren"""
    try:
        movie = Movie.objects.get(pk=pk)
    except Movie.DoesNotExist:
        return JsonResponse({'error': 'Movie not found'}, status=404)
    
    data, error = parse_json_body(request)
    if error:
        return error
    
    movie.title = data.get('title', movie.title)
    movie.year = data.get('year', movie.year)
    movie.genre = data.get('genre', movie.genre)
    movie.rating = data.get('rating', movie.rating)
    movie.description = data.get('description', movie.description)
    
    try:
        movie.save()
        return JsonResponse(movie_to_dict(movie))
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=400)


@csrf_exempt
@require_http_methods(["DELETE"])
def movie_delete(request, pk):
    """DELETE /api/movies//delete/ - Film löschen"""
    try:
        movie = Movie.objects.get(pk=pk)
        movie.delete()
        return JsonResponse({}, status=204)
    except Movie.DoesNotExist:
        return JsonResponse({'error': 'Movie not found'}, status=404)

📄 Komplette utils.py - Übersicht

"""
movies/utils.py - Helper-Funktionen für manuelle API
"""
import json
from django.http import JsonResponse


def movie_to_dict(movie):
    """Konvertiert Movie-Objekt zu Dictionary"""
    return {
        'id': movie.id,
        'title': movie.title,
        'year': movie.year,
        'genre': movie.genre,
        'rating': float(movie.rating) if movie.rating else None,
        'description': movie.description,
        'created_at': movie.created_at.isoformat(),
        'updated_at': movie.updated_at.isoformat(),
    }


def artist_to_dict(artist):
    """Konvertiert Artist-Objekt zu Dictionary"""
    return {
        'id': artist.id,
        'first_name': artist.first_name,
        'last_name': artist.last_name,
        'full_name': artist.full_name,
        'birth_date': artist.birth_date.isoformat() if artist.birth_date else None,
        'nationality': artist.nationality,
        'biography': artist.biography,
        'created_at': artist.created_at.isoformat(),
        'updated_at': artist.updated_at.isoformat(),
    }


def casting_to_dict(casting):
    """Konvertiert MovieCasting-Objekt zu Dictionary"""
    return {
        'id': casting.id,
        'movie': movie_to_dict(casting.movie),
        'artist': artist_to_dict(casting.artist),
        'role_name': casting.role_name,
        'is_main_role': casting.is_main_role,
        'order': casting.order,
        'created_at': casting.created_at.isoformat(),
    }


def parse_json_body(request):
    """
    Parst JSON aus request.body
    Returns: (data, error_response)
    """
    try:
        body = request.body.decode('utf-8')
        data = json.loads(body)
        return data, None
    except json.JSONDecodeError:
        error = JsonResponse({'error': 'Invalid JSON'}, status=400)
        return None, error
    except Exception as e:
        error = JsonResponse({'error': str(e)}, status=400)
        return None, error

🎭 Bonus: Artist API hinzufügen

In views.py ergänzen:

from .utils import artist_to_dict


@require_http_methods(["GET"])
def artist_list(request):
    """GET /api/artists/ - Liste aller Künstler"""
    artists = Artist.objects.all()
    data = [artist_to_dict(artist) for artist in artists]
    return JsonResponse(data, safe=False)


@require_http_methods(["GET"])
def artist_detail(request, pk):
    """GET /api/artists// - Ein bestimmter Künstler"""
    try:
        artist = Artist.objects.get(pk=pk)
        return JsonResponse(artist_to_dict(artist))
    except Artist.DoesNotExist:
        return JsonResponse({'error': 'Artist not found'}, status=404)


@csrf_exempt
@require_http_methods(["POST"])
def artist_create(request):
    """POST /api/artists/create/ - Neuen Künstler erstellen"""
    data, error = parse_json_body(request)
    if error:
        return error
    
    required = ['first_name', 'last_name']
    for field in required:
        if field not in data:
            return JsonResponse({'error': f'{field} is required'}, status=400)
    
    try:
        from datetime import datetime
        birth_date = None
        if 'birth_date' in data:
            birth_date = datetime.fromisoformat(data['birth_date']).date()
        
        artist = Artist.objects.create(
            first_name=data['first_name'],
            last_name=data['last_name'],
            birth_date=birth_date,
            nationality=data.get('nationality', ''),
            biography=data.get('biography', '')
        )
        return JsonResponse(artist_to_dict(artist), status=201)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=400)

In urls.py ergänzen:

urlpatterns = [
    # ... Movie URLs ...
    
    # Artist API Endpoints
    path('artists/', views.artist_list, name='artist-list'),
    path('artists/create/', views.artist_create, name='artist-create'),
    path('artists//', views.artist_detail, name='artist-detail'),
]

🔧 Error Handling verbessern

Standard Error-Responses in utils.py:

def error_response(message, status=400):
    """Standard Error Response"""
    return JsonResponse({'error': message}, status=status)


def validation_error_response(errors):
    """Validation Error Response mit Details"""
    return JsonResponse({'errors': errors}, status=400)


def not_found_response(resource='Resource'):
    """404 Response"""
    return JsonResponse(
        {'error': f'{resource} not found'},
        status=404
    )

Verwendung in Views:

from .utils import error_response, not_found_response, validation_error_response


def movie_detail(request, pk):
    try:
        movie = Movie.objects.get(pk=pk)
        return JsonResponse(movie_to_dict(movie))
    except Movie.DoesNotExist:
        return not_found_response('Movie')


def movie_create(request):
    data, error = parse_json_body(request)
    if error:
        return error
    
    # Validierung
    errors = {}
    if 'title' not in data:
        errors['title'] = 'This field is required'
    if 'year' not in data:
        errors['year'] = 'This field is required'
    elif not isinstance(data['year'], int):
        errors['year'] = 'Must be an integer'
    
    if errors:
        return validation_error_response(errors)
    
    # ... erstellen ...

🔍 Filtering & Searching hinzufügen

Query-Parameters auswerten:

@require_http_methods(["GET"])
def movie_list(request):
    """
    GET /api/movies/ - Liste aller Filme
    
    Query-Parameters:
    - year: Filme aus bestimmtem Jahr
    - genre: Filme eines Genres
    - search: Suche im Titel
    - ordering: Sortierung (-year, title, rating)
    """
    # Alle Filme
    movies = Movie.objects.all()
    
    # Filter: Jahr
    year = request.GET.get('year')
    if year:
        movies = movies.filter(year=year)
    
    # Filter: Genre
    genre = request.GET.get('genre')
    if genre:
        movies = movies.filter(genre__icontains=genre)
    
    # Search: Titel
    search = request.GET.get('search')
    if search:
        movies = movies.filter(title__icontains=search)
    
    # Ordering
    ordering = request.GET.get('ordering', '-year')
    movies = movies.order_by(ordering)
    
    # Konvertieren
    data = [movie_to_dict(movie) for movie in movies]
    
    return JsonResponse(data, safe=False)


# Beispiele:
# /api/movies/?year=1999
# /api/movies/?genre=Sci-Fi
# /api/movies/?search=matrix
# /api/movies/?ordering=title
# /api/movies/?year=2010&genre=Action&ordering=-rating

📄 Pagination hinzufügen

Manuelle Pagination:

from django.core.paginator import Paginator, EmptyPage


@require_http_methods(["GET"])
def movie_list(request):
    """
    GET /api/movies/?page=1&page_size=10
    """
    # Alle Filme
    movies = Movie.objects.all()
    
    # ... Filter anwenden ...
    
    # Pagination
    page_size = int(request.GET.get('page_size', 20))
    page_number = int(request.GET.get('page', 1))
    
    paginator = Paginator(movies, page_size)
    
    try:
        page_obj = paginator.get_page(page_number)
    except EmptyPage:
        return JsonResponse({'error': 'Page not found'}, status=404)
    
    # Response mit Meta-Daten
    data = {
        'count': paginator.count,
        'page': page_number,
        'page_size': page_size,
        'total_pages': paginator.num_pages,
        'next': page_obj.has_next() and page_obj.next_page_number() or None,
        'previous': page_obj.has_previous() and page_obj.previous_page_number() or None,
        'results': [movie_to_dict(movie) for movie in page_obj]
    }
    
    return JsonResponse(data)


# Response:
# {
#   "count": 150,
#   "page": 1,
#   "page_size": 20,
#   "total_pages": 8,
#   "next": 2,
#   "previous": null,
#   "results": [...]
# }

🔗 Nested Relationships - Movie mit Castings

Movie mit Castings zurückgeben:

def movie_to_dict_detailed(movie):
    """Movie mit allen Castings"""
    return {
        'id': movie.id,
        'title': movie.title,
        'year': movie.year,
        'genre': movie.genre,
        'rating': float(movie.rating) if movie.rating else None,
        'description': movie.description,
        'created_at': movie.created_at.isoformat(),
        'updated_at': movie.updated_at.isoformat(),
        
        # Nested Castings
        'castings': [
            {
                'id': casting.id,
                'artist': {
                    'id': casting.artist.id,
                    'full_name': casting.artist.full_name,
                },
                'role_name': casting.role_name,
                'is_main_role': casting.is_main_role,
                'order': casting.order,
            }
            for casting in movie.castings.all()
        ]
    }


@require_http_methods(["GET"])
def movie_detail_with_castings(request, pk):
    """GET /api/movies//full/ - Film mit allen Castings"""
    try:
        movie = Movie.objects.prefetch_related('castings__artist').get(pk=pk)
        return JsonResponse(movie_to_dict_detailed(movie))
    except Movie.DoesNotExist:
        return not_found_response('Movie')


# Response:
# {
#   "id": 1,
#   "title": "The Matrix",
#   "year": 1999,
#   ...
#   "castings": [
#     {
#       "id": 1,
#       "artist": {
#         "id": 1,
#         "full_name": "Keanu Reeves"
#       },
#       "role_name": "Neo",
#       "is_main_role": true,
#       "order": 1
#     },
#     ...
#   ]
# }

⚠️ Probleme der manuellen API

1️⃣

Viel wiederholender Code

# Für jedes Model:
- movie_to_dict()
- movie_list()
- movie_detail()
- movie_create()
- movie_update()
- movie_delete()

# Artist: Alles nochmal!
# MovieCasting: Alles nochmal!

→ 100+ Zeilen Code pro Model!
2️⃣

Fehleranfällig

# Vergessen zu validieren?
# Falscher Status-Code?
# Fehler nicht abgefangen?
# Edge Cases übersehen?

→ Viele potenzielle Bugs!
3️⃣

Keine Standardisierung

# Jeder Entwickler macht es anders
# Keine konsistente Error-Struktur
# Keine automatische Dokumentation
# Keine Versionierung

→ Schwer wartbar!
4️⃣

Performance-Probleme

# N+1 Queries
# Keine automatische Optimierung
# Manuelles select_related nötig

→ Langsam bei vielen Daten!
5️⃣

Fehlende Features

# Keine Auto-Dokumentation (Swagger)
# Kein Browsable API
# Keine Permissions-System
# Keine Authentication
# Kein Throttling

→ Alles manuell bauen!
6️⃣

Skalierbarkeit

# 10 Models = 600+ Zeilen Code
# Jede Änderung = Viel Arbeit
# Tests für alles schreiben

→ Nicht skalierbar!

⚖️ Vergleich: Manuell vs Django REST Framework

❌ Manuelle API (Unser Code)

# views.py (60+ Zeilen)
def movie_list(request):
    movies = Movie.objects.all()
    data = [movie_to_dict(m) for m in movies]
    return JsonResponse(data, safe=False)

def movie_detail(request, pk):
    try:
        movie = Movie.objects.get(pk=pk)
        return JsonResponse(movie_to_dict(movie))
    except Movie.DoesNotExist:
        return JsonResponse({'error': '...'}, status=404)

def movie_create(request):
    data, error = parse_json_body(request)
    if error: return error
    # ... Validierung ...
    # ... Erstellen ...
    # ... 15+ Zeilen ...

# ... update, delete ...

# utils.py (30+ Zeilen)
def movie_to_dict(movie):
    return {
        'id': movie.id,
        'title': movie.title,
        # ... 10+ Zeilen ...
    }

# urls.py (10+ Zeilen)
urlpatterns = [
    path('movies/', ...),
    path('movies/create/', ...),
    # ...
]

# = 100+ Zeilen Code
# × 3 Models = 300+ Zeilen!

✅ Django REST Framework

# serializers.py (10 Zeilen)
from rest_framework import serializers
from .models import Movie

class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'


# views.py (5 Zeilen)
from rest_framework import viewsets

class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer


# urls.py (5 Zeilen)
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('movies', MovieViewSet)
urlpatterns = router.urls

# = 20 Zeilen Code
# × 3 Models = 60 Zeilen!

# + Automatisch:
# - Browsable API
# - Swagger Docs
# - Permissions
# - Pagination
# - Filtering
# - Validation
# - Error Handling

🚀 Was Django REST Framework automatisiert

1️⃣

Serialisierung

# Manuell:
def movie_to_dict(movie):
    return {
        'id': movie.id,
        'title': movie.title,
        # ... 10 Zeilen ...
    }

# DRF:
class MovieSerializer(serializers.ModelSerializer):
    class Meta:
        model = Movie
        fields = '__all__'

# ✅ Automatisch!
2️⃣

CRUD-Operationen

# Manuell:
def movie_list(request): ...     # 10 Zeilen
def movie_detail(request, pk): ...  # 10 Zeilen
def movie_create(request): ...   # 20 Zeilen
def movie_update(request, pk): ...  # 20 Zeilen
def movie_delete(request, pk): ...  # 10 Zeilen

# DRF:
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.all()
    serializer_class = MovieSerializer

# ✅ Alle 5 Operationen automatisch!
3️⃣

Validation

# Manuell:
if 'title' not in data:
    return JsonResponse({'error': '...'}, status=400)
if not isinstance(data['year'], int):
    return JsonResponse({'error': '...'}, status=400)
# ... 20+ Zeilen ...

# DRF:
# ✅ Automatisch durch Serializer!
# ✅ Basierend auf Model-Definition!
4️⃣

Error Handling

# Manuell:
try:
    movie = Movie.objects.get(pk=pk)
except Movie.DoesNotExist:
    return JsonResponse({'error': '...'}, status=404)
except Exception as e:
    return JsonResponse({'error': str(e)}, status=500)

# DRF:
# ✅ Automatisch!
# ✅ Konsistente Error-Struktur!
5️⃣

Browsable API

# Manuell:
# ❌ Nur JSON
# ❌ Kein UI

# DRF:
# ✅ Interaktives Web-Interface!
# ✅ Formulare zum Testen!
# ✅ API-Dokumentation!
6️⃣

Swagger/OpenAPI

# Manuell:
# ❌ Keine Auto-Dokumentation
# ❌ Manuell schreiben

# DRF:
pip install drf-yasg
# ✅ Automatische Swagger-Docs!
# ✅ OpenAPI 3.0 Schema!
7️⃣

Pagination

# Manuell:
paginator = Paginator(movies, 20)
page = paginator.get_page(page_number)
data = {
    'count': paginator.count,
    'results': [...]
}

# DRF:
class MovieViewSet(viewsets.ModelViewSet):
    pagination_class = PageNumberPagination

# ✅ Automatisch!
8️⃣

Filtering & Searching

# Manuell:
if request.GET.get('year'):
    movies = movies.filter(year=...)
if request.GET.get('search'):
    movies = movies.filter(title__icontains=...)
# ... 20+ Zeilen ...

# DRF:
class MovieViewSet(viewsets.ModelViewSet):
    filter_backends = [filters.SearchFilter]
    search_fields = ['title', 'description']

# ✅ Automatisch!
1 / 4