JSON API with App Engine and Datastore

December 31, 2017
appengine datastore flask

If you are just starting with App Engine on Google Cloud Platform or evaluating it as PAAS chances are that you probably won’t have much traffic in the beginning. You will probably also need some kind of storage. If you need to store your data you might as well go with Google’s default storage suggestion for App Engine - Google Cloud Datastore.

Lets code up a simple JSON API using Flask and datastore as a storage option.

Very Brief Intro to Cloud Datastore

The Datastore is Google’s NoSQL database storage. It’s schemaless meaning you can store about any object structure including complex nested properties in form of other objects. Most of the properties are indexed by default which allows you to use them in your queries.

When using Datastore with App Engine Standard environment with Python you have to use NDB Client library. It comes with integrated automatic caching using App Engine’s Memcache which is pretty nice since it will save you some of your read quota and speed up some common queries if you have a heavy read application.

The Code

Most of the boilerplate code is the same as in previous flask tutorial which you can find here.

The JSON API we will create is a headless blog engine where users can create, update, read and delete blog posts.

Lets start with our model. It’s a simple model just to demonstrate how you would use datastore objects in an app.

class Post(ndb.Model):
    author = ndb.StringProperty()
    title = ndb.StringProperty(indexed=False)
    # slug is a computed property and is stored everytime the entity
    # is created or updated
    slug = ndb.ComputedProperty(lambda self: slugify(self.title))
    body = ndb.TextProperty()
    tags = ndb.StringProperty(repeated=True)
    # set value on create only
    created_at = ndb.DateTimeProperty(auto_now_add=True)
    # update value every time the entity is updated
    updated_at = ndb.DateTimeProperty(auto_now=True)

We need to serialize datastore objects to JSON. Once again we turn to the trusty Marshmallow lib.

class PostSerializer(Schema):
    # return url-encoded version of id
    id = fields.Function(lambda obj: obj.key.urlsafe())
    class Meta:
        fields = (
            "id", "author", "title", "slug",
            "body", "tags", "created_at", "updated_at"
        )

Here is the requirements.txt file.

Flask==0.12.2
werkzeug==0.12.2
marshmallow
python-slugify==1.2.4

Finally, here is our main.py app file.

from flask import Flask, request, jsonify, abort
from google.appengine.ext import ndb
from marshmallow import Schema, fields
from slugify import slugify
import werkzeug
import logging

app = Flask(__name__)
app.debug = True

# Our Datastore model
class Post(ndb.Model):
    author = ndb.StringProperty()
    title = ndb.StringProperty(indexed=False)
    # slug is a computed property and is stored everytime the entity
    # is created or updated
    slug = ndb.ComputedProperty(lambda self: slugify(self.title))
    body = ndb.TextProperty()
    tags = ndb.StringProperty(repeated=True)
    # set value on create only
    created_at = ndb.DateTimeProperty(auto_now_add=True)
    # update value every time the entity is updated
    updated_at = ndb.DateTimeProperty(auto_now=True)

# Marshmallow schema needed for JSON serialization
class PostSerializer(Schema):
    # return url-encoded version of id
    id = fields.Function(lambda obj: obj.key.urlsafe())
    class Meta:
        fields = (
            "id", "author", "title", "slug",
            "body", "tags", "created_at", "updated_at"
        )

post_schema = PostSerializer()
posts_schema = PostSerializer(many=True)

@app.route('/')
def index():
    return "json api with flask and datastore"

@app.route('/posts.create', methods = ['POST'])
def create():
    # allow only json
    if not request.is_json:
        abort(400, "json only please")

    post = Post(
        author=request.json['author'],
        title=request.json['title'],
        body=request.json['body'],
        tags=request.json['tags'])

    # write to datastore
    post.put()

    # return as json
    return jsonify(post_schema.dump(post).data)

@app.route('/posts.fetch')
def fetch():
    """ fetch all posts """
    posts = Post.query().fetch()

    # return as json
    return jsonify({ 'items': posts_schema.dump(posts).data })

@app.route('/posts.get')
def get():
    """ get individual post """
    post_id = request.args.get('id')
    if post_id == None:
        return abort(400, "please provide a post id")

    # get post by id
    try:
        key = ndb.Key(urlsafe=post_id)
        post = key.get()

        if post == None:
            return not_found("post was not found")

        # return as json
        return jsonify(post_schema.dump(post).data)
    except Exception, e:
        return abort(500, e)


@app.route('/posts.delete', methods = ['POST'])
def delete():
    """ delete a post """
    try:
        key = ndb.Key(urlsafe=request.json['id'])
        key.delete()
        return jsonify({'status': 'ok'}), 200
    except Exception, e:
        return abort(500, e)

@app.route('/posts.update', methods = ['POST'])
def update():
    """ update a post """
    # allow only json
    if not request.is_json:
        abort(400, "json only please")

    try:
        key = ndb.Key(urlsafe=request.json['id'])
        post = key.get()
        # if no entity with given id exists
        # return a 404 not found
        if post == None:
            return not_found("post was not found")

        json = request.json or {}

        if 'author' in json:
            post.author = json['author']
        if 'title' in json:
            post.title  = json['title']
        if 'body' in json:
            post.author = json['body']
        if 'tags' in json:
            post.tags = json['tags']
        post.put()

        return jsonify(post_schema.dump(post).data)
    except Exception, e:
        return abort(400, e)

def not_found(message='resource was not found'):
    return jsonify({
        'http_status': 404,
        'code': 'not_found',
        'message': message
    }), 404

@app.errorhandler(werkzeug.exceptions.BadRequest)
def bad_request(e):
    return jsonify({
        'http_status': 400,
        'code': 'bad_request',
        'message': '{}'.format(e)
    }), 400

@app.errorhandler(werkzeug.exceptions.InternalServerError)
def application_error(e):
    logging.exception(e)
    return jsonify({
        'http_status': 500,
        'code': 'internal_server_error',
        'message': '{}'.format(e.description)
    }), 500

Test the API

Your app engine dev environment comes with the datastore emulator out of the box and you can view your data if you visit admin server at http://localhost:8000 once the dev server is up and running.

$ dev_appserver.py --log_level debug .

Lets shoot some curls at our API. Lets create a post.

$ curl -X POST -H 'Content-Type: application/json' -d \
'{
  "author": "bob",
  "title": "Master Google Datastore",
  "body": "Should not be that hard",
  "tags": ["gae", "flask", "datastore", "gcp"]
}' http://localhost:8080/posts.create

Response should look something like this.

{
  "author": "bob",
  "body": "Should not be that hard",
  "created_at": "2017-12-31T09:47:00.539361+00:00",
  "id": "aghkZXZ-Tm9uZXIRCxIEUG9zdBiAgICAgICACgw",
  "slug": "master-google-datastore",
  "tags": [
    "gae",
    "flask",
    "datastore",
    "gcp"
  ],
  "title": "Master Google Datastore",
  "updated_at": "2017-12-31T09:47:00.542255+00:00"
}

Nice! It works! You can try some other endpoints by yourself.

  • Get by id GET http://localhost:8080/posts.get?id={post_id}
  • Fetch all GET http://localhost:8080/posts.fetch
  • Delete post POST http://localhost:8080/posts.delete
  • Update post POST http://localhost:8080/posts.update

Conclusion

Datastore is something you get out of the box if you use App Engine. Your local development environment comes with a datastore emulator (NOTE: you might have to install it separately with gcloud components install cloud-datastore-emulator). Although you must invest some time to learn the datastore it might be well worth it if you are just getting started. By using it you can concentrate on you business logic and let the Google take care of infrastructure. It might also save you some money if your app doesn’t have that much traffic yet and you don’t want to pay for a separate Cloud SQL instance.

Like what you just read? Sign up for the newsletter!

Read more