Darren O'Neill

Building an API with Django and GAE Cloud Endpoints

Typically when you build a Django site and you need to expose your application's data in the form of a RESTful API you would use something like the excellent Django REST Framework. However if you are hosting your site on Google App Engine there is an alternative called Google Cloud Endpoints that you should check out.

Cloud Endpoints is a robust solution built on Google architecture that powers various Google APIs. It takes care of a lot of the hard work for you such as authentication via OAuth 2 and JSON templating and provides an explorer you can use to test your API in the browser.

An Endpoints API is a remote procedure call (RPC) service that provides remote methods accessible to external clients. Each Endpoints API consists of an RPC service class that subclasses the ProtoRPC remote.Service class, and one or more methods. When you define a method, you must also define message classes for the requests coming into that method and the responses returned by it. A message class performs a mapping function so the incoming data can be extracted and supplied to the service method properly, or supplied properly to the outgoing response - (http://goo.gl/PrLm89)

Suppose you have your site up and running on App Engine, the project has two Django models called Book and PublicationYear and you need to create an API endpoint to list all books published in a particular year. Here is how you could go about it using Cloud Endpoints.

To get started create a new directory where your Django apps reside and call it api. In here create three files: messages.py, services.py and books_api.py. Finally create the handler for the API in your app.yaml file and ensure the DJANGO_SETTINGS_MODULE environment variable is set.

- url: /_ah/spi/.*
  script: api.books_api.application

env_variables:
  DJANGO_SETTINGS_MODULE: 'settings'

In messages.py create the ProtoRPC message classes. In this example a Book class should be created (if listing of publication years was also required, a PublicationYear class would also be needed). Rule of thumb is one Django model = one message class.

from protorpc import messages


class Book(messages.Message):
    """Book ProtoRPC message

    Book fields needed to define the schema for methods.
    """
    title = messages.StringField(1)
    author = messages.StringField(2)
    ebook_available = messages.BooleanField(3)
    publication_year = messages.IntegerField(4)
    #...

class BookCollection(messages.Message):
    """Collection of Books."""
    books = messages.MessageField(Book, 1, repeated=True)
    year = messages.IntegerField(2)

In books_api.py create each endpoint. Below the book listing view is provided.

"""Books API implemented using Google Cloud Endpoints."""

import settings
import endpoints
from protorpc import messages
from protorpc import message_types
from protorpc import remote
from books.models import Book
from api import services
from api.messages import BookCollection


package = 'Api'


@endpoints.api(name='books', version='v1',
               allowed_client_ids=[settings.GOOGLE_OAUTH2_CLIENT_ID,
                                   endpoints.API_EXPLORER_CLIENT_ID],
               scopes=[endpoints.EMAIL_SCOPE, ])
class BooksApi(remote.Service):
    """Books API v1."""

    PUBLICATION_YEAR_RESOURCE = endpoints.ResourceContainer(
        message_types.VoidMessage,
        year=messages.IntegerField(1, variant=messages.Variant.INT32))

    @endpoints.method(PUBLICATION_YEAR_RESOURCE, BookCollection,
                      path='list', http_method='GET',
                      name='books.list')
    def book_list(self, request):
        """List view endpoint."""

        if hasattr(request, 'year'):
            book_list = Book.objects.filter(
                publication_year__year=request.year)
        else:
            book_list = Book.objects.all()

        books = services.ApiUtils.serialize_books(book_list)
        return BookCollection(books=books)


application = endpoints.api_server([BooksApi], restricted=False)

The endpoints and protorpc packages are provided by the GAE Python SDK.

Authentication is required to access this endpoint, shown by the inclusion of the allowed_client_ids param. Make sure your OAuth settings are in your project settings.py file for this to work. endpoints.API_EXPLORER_CLIENT_ID can be included as an option if you wish to use the API explorer at http://yourappname.appspot.com/_ah/api/explorer.

The resource container PUBLICATION_YEAR_RESOURCE passed to the endpoint allows for the publication year to be provided as part of the request querystring, for example /_ah/api/books/v1/list?year=2005 will return all books published in 2005.

In services.py create any additional utility classes required. In the code snippet above ApiUtils.serialize_books() is referenced.

from django.forms.models import model_to_dict
from api.messages import Book


class ApiUtils(object):
"""Utility API functions."""

    @staticmethod
    def serialize_books(books):
        """Function to serialize a queryset of Book models.

        Args:
            books: a queryset
        Returns:
            A list of Book ProtoRPC messages
        """

        items = []
        for book in books:
            item = model_to_dict(book, fields=[
                'title', 'author', 'ebook_available'])
            item['publication_year'] = book.publication_year.year
            items.append(Book(**item))

        return items

In ApiUtils you could also add some authorization functions if required, i.e. does the authenticated user have an @yourdomain.com email address or are they in an approved list of users. Example:

user = endpoints.get_current_user()
if not user.email().endswith('@yourdomain.com'):
    raise endpoints.UnauthorizedException('Unauthorized user.')

That's all that is required to get an API up and running using your existing Django models.

You could improve this solution by using the Django paginator (django.core.paginator.Paginator) rather than returning all the books in one request. You would need to create a PAGINATION_TOKEN_RESOURCE and pass this into the endpoint so the user can provide a page number / token query param for each request.