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
- Tests should cover all API endpoints
- Leverage DRF's
APITestCase
and be sure to usemock
to mock external API calls - Check that valid data is returned for 201 or 200 responses
- Be sure to include tests that cover all possible error conditions
- Make sure the proper error codes/messages are being returned for 4xx responses
- Add unit tests for any complicated functions wherever appropriate
- Remember, there's no such thing as "too many tests"