Django REST Framework-Specific Standards

These are some best-practices for using DRF with Django to create a RESTful API.

Authorization

All APIs should be served via SSL and use some form of authentication, and Vokal typically prefers token authentication. Most endpoints will require authentication, and will return a 401 (Unauthorized) without it. Some endpoints—usually those used for registering new accounts—won't require authentication. If you deviate from this standard, be sure to fully document it in the project's README.md file.

Standard View Methods

If you are overriding actions on particular HTTP methods of a concrete generic view—e.g. RetrieveUpdateDestroyAPIView—you should use get(), post(), put(), patch(), and delete().

Don't use create(), list(), retrieve(), update(), partial_update(), or destroy() unless you are overriding one of DRF's base mixins to create your own.

API View organization

Create an api app inside you project. Its folder should contain one file for each app, labeled appname_api.py. All your APIView classes for that app belong in this file. You can then import these files into urls.py, and all your view classes wired up to API endpoints. A sample urls.py file is below:

from django.conf.urls import patterns
from django.conf.urls import url

import user_api
import rack_api
import photo_api
import size_api


urlpatterns = patterns('',
    url(r'^user/register/tioo/?$', user_api.RegisterTioo.as_view(), name='register_tioo'),  
    url(r'^user/login/?$', user_api.LoginTioo.as_view(), name='login_tioo'),
    url(r'^user/logout/?$', user_api.LogoutTioo.as_view(), name='logout_tioo'),
    url(r'^user/reset_request/?$', user_api.RequestPasswordReset.as_view(), name='request_pass_reset'),
    url(r'^user/reset_password/?$', user_api.ResetPassword.as_view(), name='reset_pass'),
    url(r'^user/?$', user_api.GetUpdateShopper.as_view(), name='get_update_user'),
    url(r'^rack/?$', rack_api.ListCreateRackItem.as_view(), name='get_rack_items'),
    url(r'^rack/(?P<pk>[0-9]+)/?$', rack_api.GetUpdateDeleteRackItem.as_view(), name='get_update_delete_rack'),
    url(r'^photo/?$', photo_api.ListCreateSharedPhoto.as_view(), name='upload_get_photo_items'),
    url(r'^size/?$', size_api.RetrieveSizeInfo.as_view(), name='get_size_info'),
)

In the main urls.py file for your project (projectname/projectname/urls.py), you can easily version your API like so:

from django.conf.urls import patterns, include, url


urlpatterns = patterns('',
    url(r'v1/', include('api.urls')),
    url(r'v2/', include('api.urls_v2')),
    ...
)

DateTimes

By default, DRF uses ISO 8601 formatting for datetimes. Unfortunately, ISO 8601 specifies multiple formats that aren't strictly compatible with RFC 3339 Internet timestamps. A compounding problem is that Postgres will record datetimes with microsecond precision, which is often unnecessary for front-end clients. However, the output format can change depending on whether or not a datetime is stored with microseconds or not.

Therefore, it is necessary to enforce a single datetime output format in settings.py like so:

REST_FRAMEWORK = {
    'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%S.%fZ',
    ...
}

This not only satisfies compliance with RFC 3339, it also ensures that all clients can expect a consistent datetime format.

Errors

If your serializer input doesn't validate (i.e. serializer.is_valid() != True), then you should return serializer.errors in your response.

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Generally speaking, if you need custom validation you should create a validator function and pass that in to your serializer when you define its fields. This will ensure that validation errors are consistently handled and reported. However, if you must perform additional validation outside of the serializer, be sure to emulate DRF's serializer.errors by listing the error message along with the field that didn't validate.

if email.split('@')[-1] != 'zombo.com':
    return Response(
        {'email': 'Not a zombo.com email address.'},
        status=status.HTTP_400_BAD_REQUEST)

Finally, for any other 4xx response code, the response should be a key, value pair where the key is 'detail' and the value is a specific error message.

try:
    user = User.objects.create(username=email, password=password)
except IntegrityError:
    return Response(
        {'detail': 'User with that email already exists.'},
        status=status.HTTP_409_CONFLICT)

While the error code should be sufficient enough for front-end clients to interact with the API, the additional error message will help during debugging both for yourself and for front-end engineers.

Tests