Skip to content

Quickstart

This guide walks you through setting up API versioning in an existing Django Ninja project. It does this by example, showing how to install crane, and create migrations in a fictional "persons" API. By the end, you'll have a versioned API that serves different schema versions to different clients.

Prerequisites

  • Python 3.12+
  • Django 6.0+
  • Django Ninja 1.5+
  • An existing Django Ninja API

Install django-ninja-crane

pip install django-ninja-crane

Configure Django

Add the middleware and app to your settings.py:

MIDDLEWARE = [
    # ... other middleware ...
    "crane.middleware.VersionedAPIMiddleware",
]

INSTALLED_APPS = [
    # ...
    "crane",
]

Use VersionedNinjaAPI

Replace your NinjaAPI instance with VersionedNinjaAPI:

# urls.py
from crane import VersionedNinjaAPI
from myapp.api import router

# Before:
# api = NinjaAPI()

# After:
api = VersionedNinjaAPI(api_label="default", app_label="myapp")
api.add_router("/persons", router)

urlpatterns = [
    path("api/", api.urls),
]

The api_label identifies this API within your app (you might have multiple APIs). If not sure, you can just fill in default. The app_label is your Django app name—if omitted, it's auto-detected.

Create Your First Migration

With your API defined, create the initial migration to capture the current schema:

python manage.py makeapimigrations myapp.default --name "Initial API"

This creates a migration file at myapp/api_migrations/default/m_0001_initial_api.py capturing your current API state as version "1". The version is what's shown to your users, the migration name is for you/other devs to understand how the API changed, in this case "Initial API" describes it well enough.

Make a Schema Change

Now let's evolve the API. Say you have a schema, PersonOut, used in response bodies. You want to change email: str to emails: list[str].

class PersonOut(Schema):
    name: str
    email: str


@router.get("/{person_id}", response=PersonOut)
def get_person(request, person_id: int) -> PersonOut:
    person = Person.objects.get(id=person_id)
    return PersonOut(name=person.name, email=person.email)

With django-ninja-crane, you can begin by applying the change to your schema code:

# Before
class PersonOut(Schema):
    name: str
    email: str


# After
class PersonOut(Schema):
    name: str
    emails: list[str]

Generate a Migration

After this, let Crane detect and record the change:

python manage.py makeapimigrations myapp.default --name "Change email to emails list"

This creates m_0002_change_email_to_emails_list.py with:

  • The schema delta (what changed)
  • Skeleton transformer functions (you'll implement these)

Implement Transformers

Open the generated migration file. You'll see skeleton transformer functions with NotImplementedError:

# In m_0002_change_email_to_emails_list.py

# ...


def downgrade_person_out(data: dict) -> dict:
    """2 -> 1: Transform response for v1 clients."""
    raise NotImplementedError


def upgrade_person_out(data: dict) -> dict:
    """1 -> 2: Transform request from v1 clients."""
    raise NotImplementedError


data_migrations = DataMigrationSet(
    schema_downgrades=[
        SchemaDowngrade("#/components/schemas/PersonOut", downgrade_person_out),
    ],
    schema_upgrades=[
        SchemaUpgrade("#/components/schemas/PersonOut", upgrade_person_out),
    ],
)

Fill in the transformers to convert between the old and new schema shapes:

def downgrade_person_out(data: dict) -> dict:
    """2 -> 1: Transform response for v1 clients."""
    emails = data.pop("emails", [])
    data["email"] = emails[0] if emails else ""
    return data


def upgrade_person_out(data: dict) -> dict:
    """1 -> 2: Transform request from v1 clients."""
    email = data.pop("email", None)
    data["emails"] = [email] if email else []
    return data

Test It

Your API now serves both versions:

# Request v2 (latest) - returns new schema
curl http://localhost:8000/api/persons/1
# {"name": "Alice", "emails": ["alice@example.com"]}

# Request v1 - returns old schema
curl -H "X-API-Version: 1" http://localhost:8000/api/persons/1
# {"name": "Alice", "email": "alice@example.com"}

Browse Versioned Docs

Visit your API's docs page (e.g., /api/docs) to see the Swagger UI with a version selector dropdown. Each version shows the schema as it appeared at that point in time.

What's Next?