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 !
Tip with Bitcoin
Tip me with Bitcoin and vote for this post!
Leave a comment