Flask provides many ways to write endpoints, but here are the conventions H4I projects should follow. Note that some of the "good" examples are later proven to be "bad", so make sure you account for all the conventions!
# bad
Not following the spec
# good
Following the spec!
create_response
util function# bad
@app.route('/users', methods = ['GET'])
def get_users():
data = {
'users': db.get('users')
}
response = {
'success': True,
'code': 200,
'message': '',
'result': data
}
return jsonify(response), status
# good
@app.route('/users', methods = ['GET'])
def get_users():
data = {
'users': db.get('users')
}
return create_response(data)
Why?
The create_response
abstracts away the nitty-gritty details and mechanics of sending a response. It ensures that all the responses sent from our API have a consistent format, which makes it easier for clients that consume it. Note that create_response
is a function that we have written internally.
data
parameter in create_response
# bad
@app.route('/users', methods = ['GET'])
def get_users():
return create_response(db.get('users'))
# good
@app.route('/users', methods = ['GET'])
def get_users():
data = {
'users': db.get('users')
}
return create_response(data)
Why?
Using descriptive keys provides a type checking mechanism for a client consuming our API. They will be able to ensure that they are getting what they expect. Read this for more details.
# bad
def get_users():
data = {
'users': db.get('users')
}
return create_response(data)
@app.route('/users', methods = ['GET'])
def users():
if request.method == 'GET':
return get_users()
# good
@app.route('/users', methods = ['GET'])
def get_users():
data = {
'users': db.get('users')
}
return create_response(data)
Why?
Although it's generally good practice to isolate functions for specific tasks, in this case, we don't want to decouple the endpoint logic with the endpoint declaration (the decorator). Doing so adds an unnecessary level of indirection.
# bad
@app.route('/users', methods = ['GET', 'POST'])
def users():
if request.method == 'GET':
return create_response({ 'users': db.get('users') })
if request.method == 'POST':
body = request.get_json()
user = db.create('users', body)
return create_response({ 'user': user }, status=201)
# good
@app.route('/users', methods = ['GET'])
def get_users():
return create_response({ 'users': db.get('users') })
@app.route('/users', methods = ['POST'])
def create_user():
body = request.get_json()
user = db.create('users', body)
return create_response({ 'user': user }, status=201)
Why?
We want to have good separation of concerns to maximize code readability. When reading a flask view file, you want to be able to quickly match an endpoint + type of request with its response logic. Grouping all the types of requests makes it harder to identify the corresponding response logic. However, we still want to maintain coupling between the endpoint logic and declaration, which is why we shouldn't take the approach that was deemed bad in #4.
<model>_id
instead of just <id>
in endpoint urls