Creating RESTful APIs with App Engine Cloud Endpoints

App Engine Cloud Endpoints is a great way to quickly and easily create JSON API endpoints. What’s not clear is how to structure your Message code to support a RESTful create-read-update-delete (CRUD) API. This article will show the basic CRUD operations for one Resource. The results can easily be adapted to support a full REST API.

To support this discussion let’s use a concrete resource for our API – a User resource. We can give our User model a few simple attributes.

User Model

A CRUD API for this resource would support a URL structure and HTTP verbs for each operation.

# Create a new user
HTTP POST /users/

# Read a user by id
HTTP GET /users/{id}

# Update a user by id
HTTP PUT /users/{id}

# Delete a user by id
HTTP DELETE /users/{id}

Given this model we can define a basic Cloud Endpoints message representing a User.

class UserMessage(messages.Message):
    id = messages.StringField(1)
    email = messages.StringField(2)
    username = messages.StringField(3)

Now we can write the C (create) portion of the CRUD API using HTTP POST and a ResourceContainer to hold the message we wish to submit to the API.

POST_RESOURCE = endpoints.ResourceContainer(UserMessage)

...

@endpoints.method(POST_RESOURCE,
                  UserMessage,
                  path='/users',
                  http_method='POST',
                  name='users.create')
def create(self, request):
    user = User(username=request.username, email=request.email)
    user.put()
    return user.to_message()

Similarly we can define the R (read) portion of the API using an HTTP GET method. To parameterize our cloud endpoint we need to add the parameter to our ResourceContainer. I’ll call it id here. The actual message type is VoidMessage because we are not passing any information in our request to the API endpoint other than the id parameter.

The response retrieves the entity from the datastore and returns it as a message.

ID_RESOURCE = endpoints.ResourceContainer(message_types.VoidMessage,
                                          id=messages.StringField(1,
                                                                  variant=messages.Variant.STRING,
                                                                  required=True))

...

@endpoints.method(ID_RESOURCE,
                  UserMessage,
                  http_method='GET',
                  path='users/{id}',
                  name='users.read')
def read(self, request):
    entity = User.get_by_id(request.id)
    if not entity:
        message = 'No User with the id "%s" exists.' % request.id
        raise endpoints.NotFoundException(message)

    return entity.to_message()

The U (update) operation uses a similar parameterized ResourceContainer to access a User given an id. We augment this request with the UserMessage which defines the content of the body of the message. The endpoint takes the content of the message and updates the entity with that content.

PUT_RESOURCE = endpoints.ResourceContainer(UserMessage,
                                           id=messages.StringField(1,
                                                                   variant=messages.Variant.STRING,
                                                                   required=True))

...

@endpoints.method(PUT_RESOURCE,
                  UserMessage,
                  http_method='PUT',
                  path='users/{id}',
                  name='users.update')
def update(self, request):
    entity = User.update_from_message(request.id, request)
    if not entity:
        message = 'No User with the id "%s" exists.' % request.id
        raise endpoints.NotFoundException(message)

    return entity.to_message()

Lastly, the D (delete) endpoint takes an identifier which we have previously defined as ID_RESOURCE. The endpoint deletes the entity referred to by that identifier and returns a VoidMessage which is converted to an HTTP 204 No Content response by the cloud endpoints API.

@endpoints.method(ID_RESOURCE,
                  message_types.VoidMessage,
                  http_method='DELETE',
                  path='users/{id}',
                  name='users.delete')
def delete(self, request):
    entity = User.get_by_id(request.id)
    if not entity:
        message = 'No User with the id "%s" exists.' % request.id
        raise endpoints.NotFoundException(message)

    entity.key.delete()
    return message_types.VoidMessage()

This basic pattern can be used with any resource that your API wishes to support and gives a basic pattern with which to build out your full API.

If you have any questions please send me an email or let me know in the comments!

comments powered by Disqus