Django API Versioning
Django API Versioning is a Python package for API versioning in Django and Django REST Framework (DRF) projects. Stop copy-pasting
path() declarations for every API version — declare an endpoint once,
and let the registry handle v1, v2, v3, and beyond.
from django.http import JsonResponse
from django_api_versioning.decorators import endpoint
@endpoint("users", version=2)
def user_list(request):
return JsonResponse({"users": []})
# With API_MIN_VERSION = 1, API_MAX_VERSION = 3, this single
# decorator registers:
#
# GET /api/v1/users/ -> user_list
# GET /api/v2/users/ -> user_list
# GET /api/v3/users/ -> user_list (with forward inheritance)
It registers versioned URL routes from a simple @endpoint decorator, helps preserve backward compatibility, and can optionally inherit unchanged endpoints into newer API versions.
It is especially useful for large APIs where many endpoints stay the same between releases. Instead of creating duplicate views or manually maintaining every route as the API grows, you can declare each endpoint once and control version routing centrally. Use app_name to group related resources by a domain or application area, while keeping each postfix focused on the resource itself.
- Package:
django-api-versioning - Source code: mojtaba-arvin/django-api-versioning
- License: MIT
Why not just use DRF's built-in versioning?
DRF ships with URLPathVersioning and friends — and they work. But they
only handle which version a request is on. You still write every
path() declaration by hand, duplicate unchanged routes, and decide
per-version which view to call.
| Feature | DRF URLPathVersioning |
django-api-versioning |
|---|---|---|
| Route registration | Manual path() per version |
Automatic via decorator |
| Backward compatibility | Manual | backward=True (default) |
| Forward inheritance (new versions reuse old views) | ❌ | ✅ Opt-in |
| Function-based views | ✅ | ✅ |
| Class-based views | ⚠️ DRF only | ✅ Django + DRF |
resolve().view_class preserved |
✅ | ✅ |
| Unversioned routes | Manual | version=None |
| Works without DRF | ❌ | ✅ |
Django reverse() support |
✅ | ✅ (highest version wins) |
If you already use DRF, you can keep it — this package handles routing, not request parsing.
Installation
Install Django API Versioning package from PyPI:
pip install django-api-versioning
Requires Django >= 5.2 and Python >= 3.11.
Django REST Framework is optional and auto-detected.
Quick Start
1. Configure Django settings
# settings.py
INSTALLED_APPS = [
# ... your other apps
"django_api_versioning",
]
API_BASE_PATH = "api/v{version}/"
API_MIN_VERSION = 1
API_MAX_VERSION = 3
API_BASE_PATH must contain the {version} placeholder.
2. Include the generated URL patterns
If your project doesn't have a root URLconf yet:
# settings.py
ROOT_URLCONF = "django_api_versioning.urls"
If it does, include them:
# project/urls.py
from django.urls import include, path
from django_api_versioning.urls import urlpatterns as api_urlpatterns
urlpatterns = [
# ... your other URL patterns
path("", include(api_urlpatterns)),
]
3. Decorate your views
from django.http import JsonResponse
from django_api_versioning.decorators import endpoint
@endpoint(
"products/",
app_name="catalog",
version=2,
view_name="catalog-products-list",
)
def product_list(request):
return JsonResponse({"results": []})
That's it. You now have:
GET /api/v1/catalog/products/
GET /api/v2/catalog/products/
Class-based views, DRF views, and path converters (<int:pk>) work
the same way:
from rest_framework.response import Response
from rest_framework.views import APIView
@endpoint("invoices/<int:invoice_id>/", app_name="billing", version=2)
class InvoiceDetailView(APIView):
def get(self, request, invoice_id):
return Response({"id": invoice_id})
Before and after
Without django-api-versioning — you maintain one path() per
version, and copy unchanged views when a new version ships:
# urls.py — manual, error-prone, hard to keep in sync
from django.urls import path
from . import views
urlpatterns = [
path("api/v1/users/", views.user_list_v1),
path("api/v2/users/", views.user_list_v2),
path("api/v3/users/", views.user_list_v3), # same body as v2
path("api/v1/orders/", views.order_list_v1),
path("api/v2/orders/", views.order_list_v2),
# ... × 150 endpoints
]
With django-api-versioning — one declaration, routing handled:
from django_api_versioning.decorators import endpoint
@endpoint("users/", version=2)
def user_list(request):
...
@endpoint("orders/", version=2)
def order_list(request):
...
Add API_FORWARD_INHERITANCE = True and the same declarations serve
v3 automatically, without redeclaring anything that hasn't changed.
Forward inheritance (the new feature)
Big APIs change in small increments. Suppose v3 changes only 8 endpoints out of 150. You don't want to redeclare the other 142.
Enable forward inheritance to let unchanged endpoints serve future versions automatically:
# settings.py
API_FORWARD_INHERITANCE = True
Now a single @endpoint("products/", version=2) declaration serves
/api/v1/..., /api/v2/..., and /api/v3/... — subject to
API_MAX_VERSION.
Opt one endpoint out
@endpoint("events/", app_name="audit", version=2, forward=False)
def audit_events(request):
...
The per-endpoint value overrides the global setting, in either direction.
Override an inherited endpoint
Keep the v1/v2 implementation, and register a separate view for v3:
@endpoint("reports/", app_name="analytics", version=2, view_name="reports")
def reports_v2(request):
return JsonResponse({"api_version": 2})
@endpoint(
"reports/",
app_name="analytics",
version=3,
backward=False,
forward=False,
view_name="reports",
)
def reports_v3(request):
return JsonResponse({"api_version": 3})
Result:
/api/v1/analytics/reports/ -> reports_v2
/api/v2/analytics/reports/ -> reports_v2
/api/v3/analytics/reports/ -> reports_v3
The registry resolves collisions by declared-version priority, so the override always wins regardless of module import order.
Configuration reference
Django settings
| Setting | Default | Purpose |
|---|---|---|
API_BASE_PATH |
"api/v{version}/" |
URL prefix template. Must contain {version}. |
API_MIN_VERSION |
1 |
Lowest API version to register. |
API_MAX_VERSION |
1 |
Highest configured API version. |
API_FORWARD_INHERITANCE |
False |
Default for whether endpoints may be inherited into newer versions. |
ROOT_URLCONF |
"django_api_versioning.urls" |
Django URLconf. Use the package one directly, or include its urlpatterns in your own. |
@endpoint arguments
| Argument | Default | Purpose |
|---|---|---|
postfix |
required | Resource path, e.g. "products/" or "products/<int:pk>/". |
app_name |
None |
Optional path segment for grouping (e.g. "catalog"). This is a URL path segment, not Django's namespace app_name. |
version |
None |
Version where this implementation is declared. None → unversioned route. |
backward |
True |
Also register for earlier versions, down to API_MIN_VERSION. |
forward |
None |
Per-endpoint override for forward inheritance. None → follow API_FORWARD_INHERITANCE. |
view_name |
None |
Django URL name for reverse(). Use stable, unique names. |
backward and forward control different directions:
backward=True→ expose this view to earlier versions.forward=True→ inherit this view into later versions.backward=False, forward=False→ pin to the specified version only.
Both affect routing, not whether the response is semantically compatible. You remain responsible for API contract design.
URL reversing
Give each route a stable view_name and use reverse() instead of
hard-coding versioned URLs:
from django.urls import reverse
url = reverse("catalog-products-list") # -> "/api/v3/catalog/products/"
If multiple registrations share a name, the registry orders routes so Django resolves the highest registered version.
FAQ
Does this work with DRF APIView?
Yes. Django's View is a base class of DRF's APIView, so the
decorator handles both. resolve().func.view_class is preserved, which
means DRF's authentication, permissions, and schema generation all
continue to work.
Does this conflict with DRF's DEFAULT_VERSIONING_CLASS?
No, but they solve different problems: DRF's versioning tells your
code which version a request is on; this package decides which view
is reachable at which URL. You can use both together.
What happens with duplicate view_names?
reverse() returns the highest-version route. Each versioned path is
still uniquely resolvable.
Can I use this with Django < 5.2?
No. Django 5.2 LTS is the minimum supported version. Django 4.2
reached end-of-life in April 2026.
How do I deprecate a version?
Remove the corresponding declarations from your code (or set
API_MIN_VERSION upward) and run your test suite. The package does not
manage deprecation policy — that's an API design decision.
Does the package require DRF?
No. DRF is optional. Pure Django projects work out of the box.
Where do I report a routing issue?
Open an issue at GitHub Issues
with your settings, endpoint declarations, expected URL patterns, and
actual behavior.
Contributing
Setup
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt && pip install -e .
pre-commit install
Running tests
pytest
pre-commit run --all-files
Docker
The Dockerfile parameterizes Python via PYTHON_VERSION and delegates
to entrypoint.sh:
docker build -t dapi .
docker run --rm dapi # tests
docker run --rm -e RUN_PRE_COMMIT=true dapi # + lint
docker build --build-arg PYTHON_VERSION=3.15 -t dapi:py315 . # other Python
docker run --rm dapi:py311
Releasing
- Bump
versioninpyproject.tomland updateCHANGELOG.md - Commit, push to
main git tag v0.2.1 && git push origin v0.2.1→ CI publishes to PyPI
Reporting issues
Include your Django and package settings, the @endpoint declarations
involved, expected vs actual URL patterns, and your Python and Django
versions.
License
Released under the MIT License.
Metadata
Release files for django-api-versioning 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_api_versioning-0.2.0.tar.gz | 23.3 kB | Without provenance | Details for django_api_versioning-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_api_versioning-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for django_api_versioning-0.2.0-py3-none-any.whl |
Total release size: 36.0 kB
Release files / django_api_versioning-0.2.0.tar.gz
| Download URL | django_api_versioning-0.2.0.tar.gz |
|---|---|
| Size | 23.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d6f43271cb82a1c04b7799cb84b3b1b69db48af60140a46f74ecc4203bc1ab58
|
|
BLAKE2b-256 checksum How to use checksums |
c64edfee7b27323b6446d81ed7b0408dd16b7fb5c62302915871b4dc15a2c51b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.2
|
Release files / django_api_versioning-0.2.0-py3-none-any.whl
| Download URL | django_api_versioning-0.2.0-py3-none-any.whl |
|---|---|
| Size | 12.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
25e365b787bc503bf7277b0a2ef496c03d190c251df371a54bcb61af5e24bc13
|
|
BLAKE2b-256 checksum How to use checksums |
8630121d0904bb7a2e70fe6638e04099c6b22758af091d5a9d5fcc940da6e134
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.15.2
|
Security for django-api-versioning 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.