Build a Backbone/Brunch/Chaplin Backend with Python Flask and MongoDB

Some tips on how to use Flask with MongoDB to build a REST Backend for Backbone/Brunch/Chaplin.

With both these tools, it's extremely easy to build a full featured REST Backend ready to use with Backbone Models/Collections. I hope these tips will help you avoid some pitfalls I've fallen into.

Set Backbone Model idAttribute to _id

By default Backbone expects an id key, but MongoDB use an _id key, so you have to change the default id attribute. You can check the Backbone documentation on the idAttribute.

var Meal = Backbone.Model.extend({
    idAttribute: "_id"
});

You can also make the change globally:

Backbone.Model.prototype.idAttribute = "_id";

If you forget to do this, when updating a model, Backbone will make a POST request instead of a PUT request because the id attribute won't be set.

Serve the index file with render_template

I use the backbone project index file as a flask template, and render it using render_template so it's possible to use flask session object and make link to custom flask view.

@app.route("/")
def index():
    return render_template("index.html")

If you use a tool like Brunch to build your backbone application, you might have an additional public directory, and if you want to use render_template with the index.html file, here is a way to make the folder available for flask:

import jinja2

# Here is how I initialize flask when using Brunch with Chaplin
app = Flask(__name__, static_folder="public", static_url_path="")

my_loader = jinja2.ChoiceLoader([
    app.jinja_loader,
    jinja2.FileSystemLoader(os.path.dirname(os.path.abspath(__file__)) + '/public'),
])
app.jinja_loader = my_loader

Custom jsonify

Flask has a little helper jsonify that create a Response object with a json mimetype, it makes use of simplejson or the default python json module.

@app.route("/api/test")
def api_test():
    return jsonify(items=["item1", "item2"])

Since jsonify "acts like a python dict", you must return something like jsonify(items=items) or jsonify(**mydict), so you must define a parse function in your backbone Collections

parse : function(resp) {
    return resp.items;
}

Also, with MongoDB Document, jsonify will throw an TypeError exception saying that ObjectId/Datetime is not JSON serializable, so I cast them to string using a custom JSONEncoder and a custom jsonify.

from datetime import datetime
from bson import ObjectId
import simplejson
from flask import Response


class MongoDocumentEncoder(simplejson.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return o.isoformat()
        elif isinstance(o, ObjectId):
            return str(o)
        return simplejson.JSONEncoder(self, o)


def mongodoc_jsonify(*args, **kwargs):
    return Response(simplejson.dumps(dict(*args, **kwargs), cls=MongoDocumentEncoder), mimetype='application/json')

Authentication

Here is how I typically handle authentication in a flask/backbone application.

First, Flask handle everything (user status stored in flask session, login, logout...), see Flask Quickstart on Sessions for a basic user authentication example, and if you are looking for a secure way to store user password, I recommend you to read this excellent article on how to store password securely using python-bcrypt.

Then, for every request (user/ajax request) I check user authentication using two differents Flask View Decorator. One for ajax request that send a 401 status code and the other for user page, that redirect to the login page.

from functools import wraps
import simplejson
from flask import Response, session, jsonify, request

def api_login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not 'username' in session:
            return Response(simplejson.dumps(dict(error="no login")), status=401, mimetype='application/json')
        return f(*args, **kwargs)
    return decorated_function

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not 'username' in session:
            return redirect(url_for('user_login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/')
@login_required
def serve_index():
    return render_template("index.html")


@app.route("/api/test")
@api_login_required
def api_test():
    return jsonify(items=["item1", "item2"])

MethodView

I randomly discovered flask MethodView and keep using it to deal with Backbone Model/Collection. If you haven't read the doc on flask view, you should read it.

Here is a simple and not secure example (In the real world, I use Schematics formerly dictshield to validate data).

from flask import request
from flask.views import MethodView
from bson import ObjectId


class ItemsAPI(MethodView):

    def get(self, item_id):
        if item_id is None:
            return mongodoc_jsonify(items=col.find())
        else:
            return mongodoc_jsonify(data=col.find_one({"_id": ObjectId(item_id)}))

    def post(self):
        col.insert(request.json)
        return mongodoc_jsonify(data=request.json)

    def delete(self, item_id):
        col.remove({"_id": ObjectId(item_id)})
        return ""

    def put(self, item_id):
        col.update({"_id": ObjectId(item_id)}, {'$set': request.json})
        return mongodoc_jsonify(data=request.json)

items_view = ItemsAPI.as_view('items_api')
app.add_url_rule('/api/items/', defaults={'item_id': None},
                 view_func=items_view, methods=['GET',])
app.add_url_rule('/api/items/', view_func=items_view, methods=['POST',])
app.add_url_rule('/api/items/<item_id>', view_func=items_view,
                 methods=['GET', 'PUT', 'DELETE'])

Don't forget that when using a MethodView, you have to decorate view by hand.

view = user_required(UserAPI.as_view('users'))

Your feedback

That's all. Please, don't hesitate if you have any suggestions or tips !

You should follow me on Twitter

Share this article

Tip with Bitcoin

Tip me with Bitcoin and vote for this post!

1FKdaZ75Ck8Bfc3LgQ8cKA8W7B86fzZBe2

Leave a comment

© Thomas Sileo. Powered by Pelican and hosted by DigitalOcean.