Ep. 29 Flask MethodView

Building & designing better APIs with Flask's MethodView | Learning Flask Ep. 29

Drop the @app.route decorator and write scalable, readable and better maintainable code in your Flask API

Decorating a route in Flask using the @app.route decorator provides a fast and convenient way to map a URL to a function in a web application, however, there is an alternative to consider, especially when it comes to designing and building an API.

The @app.route decorator

If you've worked with Flask before, it's inevitable that you've used the @app.route decorator to map a URL to a function in your web app, as illustrated in this example:

@app.route("/api/helloWorld")
def api_hello_world():
    # Logic goes here ...
    return make_response(jsonify({"Hello": "world"}), 200)

Convenient and fast, this method provides an easy interface for clients to interract with your application, however in my humbple opinion, not the most efficient or maintainable way to do it.

Fortunately, there is a better way. MethodView.

Maintainability

Before we jump into Flask's MethodView feature, consider the following scenario..

You're building an API using Flask and you need to create an endpoint with the same url to handle 5 different request methods (GET, POST, PUT, PATCH and DELETE)

For this demonstration, the URL is /api/example/<entity>, passing a value for entity in the URL to the function.

Using the @app.route decorator, you could achieve this with one of the following designs:

The inefficient way - Create a route for each of the required request methods:

@app.route("/api/example/<entity>", methods=["GET"])
def api_example_get(entity):
    """ Responds to GET requests """
    return "Responding to a GET request"

@app.route("/api/example/<entity>", methods=["POST"])
def api_example_post(entity):
    """ Responds to POST requests """
    return "Responding to a POST request"

@app.route("/api/example/<entity>", methods=["PUT"])
def api_example_put(entity):
    """ Responds to PUT requests """
    return "Responding to a PUT request"

@app.route("/api/example/<entity>", methods=["PATCH"])
def api_example_patch(entity):
    """ Responds to PATCH requests """
    return "Responding to a PATCH request"

@app.route("/api/example/<entity>", methods=["DELETE"])
def api_example_delete(entity):
    """ Responds to DELETE requests """
    return "Responding to a DELETE request"

The unmaintainable way - Request method logic handled inside the function:

from flask import request

@app.route("/api/example/<entity>", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
def api_example_all_methods(entity):
    """ Request method logic handled inside the function """
    if request.method == "GET":
        """ Respond to the GET request """
        return "Responding to a GET request"
    if request.method == "POST":
        """ Respond to the POST request """
        return "Responding to a POST request"
    if request.method == "PUT":
        """ Respond to the PUT request """
        return "Responding to a PUT request"
    if request.method == "PATCH":
        """ Respond to the PATCH request """
        return "Responding to a PATCH request"
    if request.method == "DELETE":
        """ Respond to the DELETE request """
        return "Responding to a DELETE request"

Why are these 2 examples not ideal?

In the first example, any changes to the URL or URL arguments will need to be refactored for all 5 corresponding routes and functions, along with the disadvantage of having 5 separate function names for the same URL (Think redirect(url_for("function_name")))

In the second example, we loose the disadvantage of having to refactor multiple URLs or parameters by only having to change it once, along with only having a sungle function name to handle all 5 methods, however the handling of the request method inside the function is verbose and unmaintainable (Imagine this scenario across hundreds of endpoints)

In the words of Raymond Hettinger, I can hear you all screaming, "There must be a better way!"

And thankfully there is...

MethodView

Here's the same logic using Flask's MethodView:

from flask.views import MethodView

class ExampleEndpoint(MethodView):
    """ Example of a class inheriting from flask.views.MethodView 

    All 5 request methods are available at /api/example/<entity>
    """
    def get(self, entity):
        """ Responds to GET requests """
        return "Responding to a GET request"

    def post(self, entity):
        """ Responds to POST requests """
        return "Responding to a POST request"

    def put(self, entity):
        """ Responds to PUT requests """
        return "Responding to a PUT request"

    def patch(self, entity):
        """ Responds to PATCH requests """
        return "Responding to a PATCH request"

    def delete(self, entity):
        """ Responds to DELETE requests """
        return "Responding to a DELETE request"

app.add_url_rule("/api/example/<entity>", view_func=ExampleEndpoint.as_view("example_api"))

I think you'll agree, this example is cleaner and much more maintainable.

To get started we need to import MethodView from flask.views, followed by creating a class which inherits from it and defining a method for each of the desired request methods. Pretty straight forward!

class Foo(MethodView):
    pass

You'll notice, each of the 5 method names defined in our ExampleEndpoint correspond to the request method required to trigger it.

Tip - You only need to define methods for the requests you want to handle

class Foo(MethodView):
    def get(self):
        return "I only get triggered by GET requests!"

Unlike the @app.route decorator (which automatically registers URLs against our application), we have to manually register the URL and class with our application using the app.add_url_rule method and passing it a few parameters:

app.add_url_rule("/api/foo", view_func=Foo.as_view("foo_api"))

The first argument is the URL, followed by view_func=YourClass.as_view("example_api")

The as_view method is inherited by your class when you inherit from MethodView and registers a new function name with the application, meaning you'll be able to reference it throughout your app, for example using url_for("foo_api")

A working example

Here's a simple, fun working example of MethodView in action:

from flask import Flask, make_response, jsonify, request, redirect, url_for
from flask.views import MethodView

app = Flask(__name__)

inventory = {
    "apple": {
        "description": "Crunchy and delicious",
        "qty": 30
    },
    "cherry": {
        "description": "Red and juicy",
        "qty": 500
    },
    "mango": {
        "description": "Red and juicy",
        "qty": 500
    }
}


class InventoryApi(MethodView):
    """ /api/inventory """

    def get(self):
        """ Return the entire inventory collection """
        return make_response(jsonify(inventory), 200)

    def delete(self):
        """ Delete the entire inventory collection """
        inventory.clear()
        print(inventory)
        return make_response(jsonify({}), 200)


class InventoryItemApi(MethodView):
    """ /api/inventory/<item_name> """

    error = {
        "itemNotFound": {
            "errorCode": "itemNotFound",
            "errorMessage": "Item not found"
        },
        "itemAlreadyExists": {
            "errorCode": "itemAlreadyExists",
            "errorMessage": "Could not create item. Item already exists"
        }
    }

    def get(self, item_name):
        """ Get an item """
        if not inventory.get(item_name, None):
            return make_response(jsonify(self.error["itemNotFound"]), 400)
        return make_response(jsonify(inventory[item_name]), 200)

    def post(self, item_name):
        """ Create an item """
        if inventory.get(item_name, None):
            return make_response(jsonify(self.error["itemAlreadyExists"]), 400)
        body = request.get_json()
        inventory[item_name] = {"description": body.get("description", None), "qty": body.get("qty", None)}
        return make_response(jsonify(inventory[item_name]))

    def put(self, item_name):
        """ Update/replace an item """
        body = request.get_json()
        inventory[item_name] = {"description": body.get("description", None), "qty": body.get("qty", None)}
        return make_response(jsonify(inventory[item_name]))

    def patch(self, item_name):
        """ Update/modify an item """
        if not inventory.get(item_name, None):
            return make_response(jsonify(self.error["itemNotFound"]), 400)
        body = request.get_json()
        inventory[item_name].update({"description": body.get("description", None), "qty": body.get("qty", None)})
        return make_response(jsonify(inventory[item_name]))

    def delete(self, item_name):
        """ Delete an item """
        if not inventory.get(item_name, None):
            return make_response(jsonify(self.error["itemNotFound"]), 400)
        del inventory[item_name]
        return make_response(jsonify({}), 200)


app.add_url_rule("/api/inventory", view_func=InventoryApi.as_view("inventory_api"))
app.add_url_rule("/api/inventory/<item_name>", view_func=InventoryItemApi.as_view("inventory_item_api"))

if __name__ == "__main__":
    app.run()

Last modified · 30 Jul 2019 Reference : https://pythonise.com/series/learning-flask/flask-api-methodview

Last updated