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!

1) Follow Our REST API Specification

# bad
Not following the spec

# good
Following the spec!

2) Use the 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.

3) Follow the format of the 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.

4) Write your endpoint logic under the route decorator

# 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.

5) Write the logic for only one request type per endpoint. You can create multiple endpoint declarations for the same endpoint url.

# 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.

6) Use <model>_id instead of just <id> in endpoint urls