django-rest-framework: api versioning

22,041

Solution 1

One way of doing this is to have the versioning specified as part of the media type.

This is what GitHub currently do for their API.

You can also include media type parameters in your accept headers, eg Accept: application/json; version=beta, which will successfully match against JSONRenderer. You can then code your view to behave differently depending on the accepted media type, see here.

There's lots of different patterns for versioning in APIs, and I wouldn't say there's any great consensus around the right approach yet, but that'd be one reasonable possibility.


Update Jan 2015: Better versioning support will be incoming in the 3.1.0 release. See [this pull request]

Update March 2015: Docs for the versioning API are now available.

(https://github.com/tomchristie/django-rest-framework/pull/2285) for more details.

Solution 2

UPDATE:

versioning is now properly supported.


There are some answers from your link:

We found it practical and useful to put the version in the URL. It makes it easy to tell what you're using at a glance. We do alias /foo to /foo/(latest versions) for ease of use, shorter / cleaner URLs, etc, as the accepted answer suggests. Keeping backwards compatibility forever is often cost-prohibitive and/or very difficult. We prefer to give advanced notice of deprecation, redirects like suggested here, docs, and other mechanisms.

So we took this approach, plus allowing clients to specify the version in request header (X-Version), here is how we did it:

Structure in side the API app:

.
├── __init__.py
├── middlewares.py
├── urls.py
├── v1
│   ├── __init__.py
│   ├── account
│   │   ├── __init__.py
│   │   ├── serializers.py
│   │   └── views.py
│   └── urls.py
└── v2
    ├── __init__.py
    ├── account
    │   ├── __init__.py
    │   ├── serializers.py
    │   └── views.py
    └── urls.py

project urls.py:

url(r'^api/', include('project.api.urls', namespace='api')),

api app level urls.py:

from django.conf.urls import *

urlpatterns = patterns('',
    url(r'', include('project.api.v2.urls', namespace='default')),
    url(r'^v1/', include('project.api.v1.urls', namespace='v1')),
)

version level urls.py

from django.conf.urls import *
from .account import views as account_views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('account', account_views.AccountView)
router.register('myaccount', account_views.MyAccountView)
urlpatterns = router.urls

create a middleware to switch to the correct code by changing the path_info, please note there is a caveat that namespace ('api') defined in project level urls is not flexible and needs to be known in middleware:

from django.core.urlresolvers import resolve
from django.core.urlresolvers import reverse


class VersionSwitch(object):

    def process_request(self, request):
        r = resolve(request.path_info)
        version = request.META.get('HTTP_X_VERSION', False)
        if r.namespace.startswith('api:') and version:
            old_version = r.namespace.split(':')[-1]
            request.path_info = reverse('{}:{}'.format(r.namespace.replace(old_version, version), r.url_name), args=r.args, kwargs=r.kwargs)

Sample url:

curl -H "X-Version: v1" http://your.domain:8000/api/myaccount/

Solution 3

@James Lin gave a great answer. In comments to the answer @Mar0ux asked what to do with broken HyperlinkedRelatedField fields.

I fixed this with changing HyperlinkedRelatedField to the SerializerMethodField and calling reverse with, very unobvious, passing extra parameter current_app to it.

For example, I have an app 'fruits_app', with namespace versions 'v1', 'v2'. And I have serializer of Fruit model. So to serialize url I create a field

url = serializers.SerializerMethodField()

And corresponding method:

def get_url(self, instance):
    reverse.reverse('fruits_app:fruit-detail',
        args=[instance.pk],
        request=request,
        current_app=request.version)

With nested namespaces you will need to change add those namespaces to current_app. For example, if you have an app 'fruits_app' with namespace versions 'v1', 'v2', and instance namespace 'bananas' inside the app, method for serializing Fruit url will look like:

def get_url(self, instance):
    reverse.reverse('fruits_app:fruit-detail',
        args=[instance.pk],
        request=request,
        current_app='bananas:{}'.format(request.version))
Share:
22,041

Related videos on Youtube

w--
Author by

w--

Updated on July 09, 2022

Comments

  • w--
    w-- almost 2 years

    so googling around it appears that the general consensus is that embedding version numbers in REST URIs is a bad practice and a bad idea.

    even on SO there are strong proponents supporting this.
    e.g. Best practices for API versioning?

    My question is about how to accomplish the proposed solution of using the accept header / content negotiation in the django-rest-framework to accomplish this.

    It looks like content negotiation in the framework,
    http://django-rest-framework.org/api-guide/content-negotiation/ is already configured to automatically return intended values based on accepted MIME types. If I start using the Accept header for custom types, I'll lose this benefit of the framework.

    Is there a better way to accomplish this in the framework?

  • w--
    w-- about 10 years
    Apparently this question has earned a popular question badge and just realized i never accepted the answer. Thanks for all your hard work on the framework Tom!
  • maroux
    maroux over 9 years
    This approach would be fine, except that it breaks hyperlinked fields (HyperlinkedRelatedField etc). Any ideas?
  • James Lin
    James Lin over 9 years
    I haven't got a project setup to use HyperlinkedRelatedField I guess your problem would be the link going to the default version if you specified a different version?
  • maroux
    maroux over 9 years
    Exactly. I am inclined to go with Accept header method of versioning so that URLs don't change at all.
  • James Lin
    James Lin over 9 years
    I am afraid there is no easy way to fix until versioning is baked into the package, so it will produce version aware urls for HyperlinkedRelatedField
  • James Lin
    James Lin over 9 years
    ... unless you like monkey patch?
  • maroux
    maroux over 9 years
    I don't like monkey patching. But I don't mind forking and adding support for versioned urls.
  • ferrangb
    ferrangb over 9 years
    What about working without namespaces and not creating the middleware?
  • Eyeball
    Eyeball about 9 years
    So where do we put the models?
  • James Lin
    James Lin about 9 years
    Your data models will be in your app folder.
  • Eduard Gamonal
    Eduard Gamonal over 8 years
    James, do you have one app per version? or one app called "api" and modules in it? do you mean having separate models per version?
  • James Lin
    James Lin over 8 years
    I have multiple apps with models in it, and one app which is the 'API' which has versions.
  • deserg
    deserg about 3 years
    @Mar0ux I fixed HyperlinkedRelatedField with making the SerializerMethodField and calling reverse with, very unobvious, passing extra parameter current_app to it. For example, I have an app 'fruits_app', with namespace versions 'v1', 'v2'. And I have serializer of Fruit model. So I create a field url = serializers.SerializerMethodField() And the reverse call would be like: def get_url(self, instance): reverse.reverse(''fruits_app:fruit", args=[instance.pk], request=request, current_app=request.version)