On the Proper Care and Feeding of REST APIs - Part I

After working the full-stack for assignment 1, and purely on the frontend for assignment 3, for the final project I get the exciting task of designing and building a backend API from scratch to power the two frontends of our final project product. I’ve only ever constructed APIs twice before, and both times with toy projects that had at most one or two endpoints. This on the other hand is significantly bigger - for the first week of the project, by GitHub’s estimation, our project repository had more API documentation than code!

Building a REST API is a big responsibility. The API is how the rest of the world talks to your application, even if the rest of the world in this case is just two client-side web apps. It should be stable and consistent so that the developers building against it have a stable foundation to build their applications on, yet flexible enough to support all operations that they can possibly need. And for this project it’s just me, while the other three members of my team hack away at the frontend.

In the first part of this post, I’ll talk about the tools used to build our API, and the reasoning behind the choice.

Documentation - Apiary and API Blueprint

Designing and documenting API is surprisingly difficult. From previous experience, even documenting a trivial API is hard work - REST APIs need to document their endpoint locations, inputs (URL and query parameters, request body), and outputs (response format, error codes, HTTP status codes). And that’s not all - modern API documentation (Stack Exchange and reddit’s, for example) need examples of requests and response, an API console to play with, and even a mock or test server to run test clients against. Apiary, the documentation tool we chose, gives us all that for free, saving us an inordinate amount of time. Check out our API documentation on Apiary.

Backend Stack - Django, Django REST Framework and PostgreSQL

Django, introduced in a previous post here, is the weapon of choice for rapid prototyping and building data and relation heavy web applications. The automatically generated schema system and helps immensely in reducing the grunt work of ensuring the state of the database matches that of the application models, and high level of abstraction reduces the amount of boilerplate that is needed.

And as it happen, Django also has the wonderful Django REST Framework (DRF) package that extends Django’s capability to allow it to act as an API server. Django REST Framework’s power lies in its design - it extends Django’s request lifecycle with a number of useful extensions, including permissions, filtering, pagination, authentication, versioning, serialization, content negotiation and response formats. Each of these are represented as classes that can be customized and reused throughout the API, and can be quickly plugged into any part of the API by simply setting the appropriate property on the View class, or through decorators when using function based views.

In addition, for common use cases DRF comes with generic views for standard CRUD operations. Here’s two simple examples from our code:

# Simple views such as the one for creating event agenda viewers can 
# be represented using method views and configured using decorators 
@api_view(('POST',))
@permission_classes((AllowAny,))
def create_viewer(request, agenda_id):
    serializer = ViewerSerializer(data=request.data, context={
        'agenda': get_object_or_404(Agenda, pk=agenda_id),
    })

    serializer.is_valid(True)
    viewer = serializer.save()
    return Response({'token': viewer.token}, status=status.HTTP_201_CREATED)

# Because class based views are Python classes, common methods can be extracted 
# to mixins. This mixin overrides APIView class's get_serializer_context method 
# to pass additional data to the serializer class 
class AgendaContextMixin:
    def get_serializer_context(self):
        agenda = get_object_or_404(Agenda, pk=self.kwargs['agenda_id'])
        return {
            **super().get_serializer_context(),
            'agenda': agenda,
                }

# A class based view using a generic view. The RetrieveUpdateDestroyAPIView  
# provides CRUD to individual objects - in this case, Tracks, binding 
# model and serializer to the appropriate HTTP verb 
class TrackDetail(AgendaContextMixin, RetrieveUpdateDestroyAPIView):
    permission_classes = (IsAgendaOwnerOrReadOnly,)
    serializer_class = serializers.TrackSerializer

    def get_queryset(self):
        return Track.objects.filter(agenda=self.kwargs['agenda_id'])

For more example, see DRF’s readme as well as the quickstart.

Next: Details and Problems Faced

Of course, the process of building the API is not plain sailing. In the next part of this post I’ll go into details about how we used each of these tools as well as talk about some of the problems faced and my solutions to them.