Designing a RESTful API using Flask-RESTful

Posted by
on under

This is the third article in which I explore different aspects of writing RESTful APIs using the Flask microframework. Here is the first, and the second.

The example RESTful server I wrote before used only Flask as a dependency. Today I will show you how to write the same server using Flask-RESTful, a Flask extension that simplifies the creation of APIs.

The RESTful server

As a reminder, here is the definition of the ToDo List web service that has been serving as an example in my RESTful articles:

HTTP MethodURIAction
GEThttp://[hostname]/todo/api/v1.0/tasksRetrieve list of tasks
GEThttp://[hostname]/todo/api/v1.0/tasks/[task_id]Retrieve a task
POSThttp://[hostname]/todo/api/v1.0/tasksCreate a new task
PUThttp://[hostname]/todo/api/v1.0/tasks/[task_id]Update an existing task
DELETEhttp://[hostname]/todo/api/v1.0/tasks/[task_id]Delete a task

The only resource exposed by this service is a "task", which has the following data fields:

  • uri: unique URI for the task. String type.
  • title: short task description. String type.
  • description: long task description. Text type.
  • done: task completion state. Boolean type.

Routing

In my first RESTful server example (source code here) I have used regular Flask view functions to define all the routes.

Flask-RESTful provides a Resource base class that can define the routing for one or more HTTP methods for a given URL. For example, to define a User resource with GET, PUT and DELETE methods you would write:

from flask import Flask
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app)

class UserAPI(Resource):
    def get(self, id):
        pass

    def put(self, id):
        pass

    def delete(self, id):
        pass

api.add_resource(UserAPI, '/users/<int:id>', endpoint = 'user')

The add_resource function registers the routes with the framework using the given endpoint. If an endpoint isn't given then Flask-RESTful generates one for you from the class name, but since sometimes the endpoint is needed for functions such as url_for I prefer to make it explicit.

My ToDo API defines two URLs: /todo/api/v1.0/tasks for the list of tasks, and /todo/api/v1.0/tasks/<int:id> for an individual task. Since Flask-RESTful's Resource class can wrap a single URL this server will need two resources:

class TaskListAPI(Resource):
    def get(self):
        pass

    def post(self):
        pass

class TaskAPI(Resource):
    def get(self, id):
        pass

    def put(self, id):
        pass

    def delete(self, id):
        pass

api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint = 'tasks')
api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint = 'task')

Note that while the method views of TaskListAPI receive no arguments the ones in TaskAPI all receive the id, as specified in the URL under which the resource is registered.

Request Parsing and Validation

When I implemented this server in the previous article I did my own validation of the request data. For example, look at how long the PUT handler is in that version:

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['PUT'])
@auth.login_required
def update_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    if not request.json:
        abort(400)
    if 'title' in request.json and type(request.json['title']) != unicode:
        abort(400)
    if 'description' in request.json and type(request.json['description']) is not unicode:
        abort(400)
    if 'done' in request.json and type(request.json['done']) is not bool:
        abort(400)
    task[0]['title'] = request.json.get('title', task[0]['title'])
    task[0]['description'] = request.json.get('description', task[0]['description'])
    task[0]['done'] = request.json.get('done', task[0]['done'])
    return jsonify( { 'task': make_public_task(task[0]) } )

Here I have to make sure the data given with the request is valid before using it, and that makes the function pretty long.

Flask-RESTful provides a much better way to handle this with the RequestParser class. This class works in a similar way as argparse for command line arguments.

First, for each resource I define the arguments and how to validate them:

from flask_restful import reqparse

class TaskListAPI(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type = str, required = True,
            help = 'No task title provided', location = 'json')
        self.reqparse.add_argument('description', type = str, default = "", location = 'json')
        super(TaskListAPI, self).__init__()

    # ...

class TaskAPI(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type = str, location = 'json')
        self.reqparse.add_argument('description', type = str, location = 'json')
        self.reqparse.add_argument('done', type = bool, location = 'json')
        super(TaskAPI, self).__init__()

    # ...

In the TaskListAPI resource the POST method is the only one the receives arguments. The title argument is required here, so I included an error message that Flask-RESTful will send as a response to the client when the field is missing. The description field is optional, and when it is missing a default value of an empty string will be used. One interesting aspect of the RequestParser class is that by default it looks for fields in request.values, so the location optional argument must be set to indicate that the fields are coming in request.json.

The request parser for the TaskAPI is constructed in a similar way, but has a few differences. In this case it is the PUT method that will need to parse arguments, and for this method all the arguments are optional, including the done field that was not part of the request in the other resource.

Now that the request parsers are initialized, parsing and validating a request is pretty easy. For example, note how much simpler the TaskAPI.put() method becomes:

    def put(self, id):
        task = filter(lambda t: t['id'] == id, tasks)
        if len(task) == 0:
            abort(404)
        task = task[0]
        args = self.reqparse.parse_args()
        for k, v in args.iteritems():
            if v != None:
                task[k] = v
        return jsonify( { 'task': make_public_task(task) } )

A side benefit of letting Flask-RESTful do the validation is that now there is no need to have a handler for the bad request code 400 error, this is all taken care of by the extension.

Generating Responses

My original REST server generates the responses using Flask's jsonify helper function. Flask-RESTful automatically handles the conversion to JSON, so instead of this:

        return jsonify( { 'task': make_public_task(task) } )

I can do this:

        return { 'task': make_public_task(task) }

Flask-RESTful also supports passing a custom status code back when necessary:

        return { 'task': make_public_task(task) }, 201

But there is more. The make_public_task wrapper from the original server converted a task from its internal representation to the external representation that clients expected. The conversion included removing the id field and adding a uri field in its place. Flask-RESTful provides a helper function to do this in a much more elegant way that not only generates the uri but also does type conversion on the remaining fields:

from flask_restful import fields, marshal

task_fields = {
    'title': fields.String,
    'description': fields.String,
    'done': fields.Boolean,
    'uri': fields.Url('task')
}

class TaskAPI(Resource):
    # ...

    def put(self, id):
        # ...
        return { 'task': marshal(task, task_fields) }

The task_fields structure serves as a template for the marshal function. The fields.Url type is a special type that generates a URL. The argument it takes is the endpoint (recall that I have used explicit endpoints when I registered the resources specifically so that I can refer to them when needed).

Authentication

The routes in the REST server are all protected with HTTP basic authentication. In the original server the protection was added using the decorator provided by the Flask-HTTPAuth extension.

Since the Resouce class inherits from Flask's MethodView, it is possible to attach decorators to the methods by defining a decorators class variable:

from flask_httpauth import HTTPBasicAuth
# ...
auth = HTTPBasicAuth()
# ...

class TaskAPI(Resource):
    decorators = [auth.login_required]
    # ...

class TaskAPI(Resource):
    decorators = [auth.login_required]
    # ...

Conclusion

The complete server implementation based on Flask-RESTful is available in my REST-tutorial project on github. The file with the Flask-RESTful server is rest-server-v2.py.

You can also download the entire project including both server implementations and a javascript client to test it:

Download REST-tutorial project.

Let me know if you have any questions in the comments below.

Miguel

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

188 comments
  • #126 rusty said

    your tutorials are really awesome.. they have been the bone of my progress in flask.
    pls can you add links to the 2 preceding articles about creating restful apps with flask.

    Thanks :)

  • #127 Bob Haffner said

    Hi Miguel,

    FYI

    I started going through the Flask-RESTful docs and noticed that they are deprecating the RequestParser and thought I would pass that along. https://flask-restful.readthedocs.io/en/latest/reqparse.html#request-parsing

    Bob

    ps Loved your Oreilly video on Building Web APIs with Flask

  • #128 Tuan Nguyen Minh said

    Hello Miguel,
    Thanks for great guidelines. Just want to find out in your restful v2, the uri is "/todo/api/v1.0/tasks/[task_id]" only, instead of full url "http://[hostname]/todo/api/v1.0/tasks/[task_id]" compare to v1, what is the point here?

    Thank you very much!

  • #129 Miguel Grinberg said

    @Tuan: this is because the URLs in this example are generated by Flask-RESTful, which by default generates relative URLs. You can switch to absolute URLs if you change fields.Url('task') to 'uri': fields.Url('task', absolute=True).

  • #130 Tuan Nguyen Minh said

    Thanks Miguel. The API is working as expected but if i supervise data packets by tcpdump/wireshark, it always show "Malformed Packet" or "Unreassembled Packet" for both request and response, regardless i start the API from pure python command or put it behind nginx + gunicorn. Could you please to take a look?

  • #131 Miguel Grinberg said

    @Tuan: there are several reasons why wireshark may have trouble decoding packets, all unrelated to the application, which works at a much higher level. See the documentation at https://www.wireshark.org/docs/wsug_html_chunked/AppMessages.html.

  • #132 gberrido said

    Hello Miguel and thanks for the awsome education material in particular for a beginner in python/flask.

    I've been working on connecting the flask-restful example to a flask-sql-alchemy DB, with very partial success till now (Add Task function works because i see the new task in the DB file).

    But otherwise the Task List doesnt seem to reach the client and I am stuck with cryptic trace back like: "werkzeug.routing.BuildError: Could not build url for endpoint 'task' with values ['task']. Did you forget to specify values ['id']?"

    *The app is initialized with the original api path:
    from resources import TaskListAPI
    from resources import TaskAPI

    api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint='tasks')
    api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint='task')

    I suppose the bug is in the API classes functions:
    the API classes are:

    task_fields = {

    'title': fields.String,
    'description': fields.String,
    'done': fields.Boolean,
    'uri': fields.Url('task', absolute=True)
    

    }

    class TaskListAPI(Resource):
    decorators = [auth.login_required]

    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type=str, required=True,
                                   help='No task title provided',
                                   location='json')
        self.reqparse.add_argument('description', type=str, default="",
                                   location='json')
        super(TaskListAPI, self).__init__()
    
    @marshal_with(task_fields)
    def get(self):
        tasks = session.query(Task).all()
        return {'tasks': [marshal(task, task_fields) for task in tasks]}
    
    @marshal_with(task_fields)
    def post(self):
        parsed_args = self.reqparse.parse_args()
        task = Task(title=parsed_args['title'], description=parsed_args['description'], done=False)
        session.add(task)
        session.commit()
        return {'task': marshal(task, task_fields)}, 201
    

    class TaskAPI(Resource):
    decorators = [auth.login_required]

    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type=str, location='json')
        self.reqparse.add_argument('description', type=str, location='json')
        self.reqparse.add_argument('done', type=bool, location='json')
        super(TaskAPI, self).__init__()
    
    @marshal_with(task_fields)
    def get(self, id):
        task = session.query(Task).filter(Task.id == id).first()
        if not task:
            abort(404, message="Task {} doesn't exist".format(id))
        return {'task': marshal(task[0], task_fields)}
    
    def delete(self, id):
        task = session.query(Task).filter(Task.id == id).first()
        if not task:
            abort(404, message="Task {} doesn't exist".format(id))
        session.delete(task)
        session.commit()
        return {'result': True}
    
    @marshal_with(task_fields)
    def put(self, id):
        parsed_args = self.reqparse.parse_args()
        task = session.query(Task).filter(Task.id == id).first()
        task.title = parsed_args['title']
        session.add(task)
        session.commit()
        return {'task': marshal(task, task_fields)}
    

    Any comment , explanation or advise on that thing is welcome :)
    What should I do to get it work properly?

  • #133 Miguel Grinberg said

    @gbarrido: without saying the full stack trace I can't really say. But it looks like you are doing double marshalling in your endpoints, you have the decorator and also calling the marshal function. You have to pick one.

  • #134 gberrido said

    Yes, that was the problem, thanks for pointing it out!

    Btw as a more general feedback, the quality of your tuto is imo because they are A to Z, integrating several techniques in one complete, working example, which is extremely useful to kick start for a newbie.

    If you ever feel like to apply your didactic talent to other flask-* combinations, here is my wish list (it's soon Xmas afterall :)

    • flask-restful API + (flask-)sqlalchemy + Mobile App client (Qt/QML for example)
    • flask-restless + (flask-)marshmallow + the awsome knockout client

    Thanks again for the efficient feedback and tutorials
    gb

  • #135 Gunnar said

    Hi,

    Can you marry this article with the blueprint-based version handling you defined in this stackoverflow response? http://stackoverflow.com/questions/28795561/support-multiple-api-versions-in-flask

    Do you still use the @api.route decorator or is the better approach to use a class name(Resource) combined with api.add_resource?

    Thanks,

    Gunnar

  • #136 Miguel Grinberg said

    @Gunnar: api.route decorator is basic Flask. The Resource class is from an extension, Flask-RESTful. Which one to use is really a matter of preference, in the end, the just use different approaches to register your endpoints.

  • #137 Gunnar Tapper said

    Thanks Miguel. Strangely, I cannot get the example to work.

    Traceback (most recent call last):
    File "test.py", line 3, in <module>
    from api.v1 import api as api_v1
    File "/home/gunnar/testapp/test/api/v1/init.py", line 5, in <module>
    from . import routes
    File "/home/gunnar/testapp/test/api/v1/routes.py", line 8, in <module>
    @api.route('/metrics/<string:metric_id>', methods = ['GET', 'PUT'])
    NameError: name 'api' is not defined
    The project organization is:

    test.py
    +api/
    +-- v1/
    +-- init.py
    +-- routes.py
    init.py contains:

    from flask import Blueprint

    api = Blueprint( 'api', name )

    from . import routes
    routes.py contains:

    from flask import request
    from flask_restful import Resource

    metrics = {}

    <h1>api is defined in init.py</h1>

    @api.route('/metrics/<string:metric_id>', methods = ['GET', 'PUT'])
    def process_metric( metric_id ):
    if request.method == 'GET':
    return { metric_id: metrics[metric_id] }

    if request.method == 'PUT':
       metrics[metric_id] = request.form['data']
       return { metric_id: metrics[metric_id] }
    

    It seems to me that it'd be better(?) to use the class approach but I don't understand how to do that when using blueprints and the versioning scheme?

  • #138 Miguel Grinberg said

    @Gunnar: you are importing "api' as "api_v1', so you should invoke the decorator as "api_v1.route".

  • #139 Gunnar Tapper said

    @Miquel

    No love.

    Traceback (most recent call last):
    File "writer.py", line 3, in <module>
    from api.v1 import api as api_v1
    File "/home/gunnar/testapp/test/api/v1/init.py", line 5, in <module>
    from . import routes
    File "/home/gunnar/testapp/test/api/v1/routes.py", line 8, in <module>
    @api_v1.route('/metrics/<string:metric_id>', methods = ['GET', 'PUT'])
    NameError: name 'api_v1' is not defined

  • #140 Miguel Grinberg said

    @Gunnar: You seem to have cyclic imports. Your writer.py module imports api.v1.api, which in turn imports api.v1.routes, which imports api.v1.api again. Fix the recursive imports and you should be fine. If you don't know how, look up my Flask at Scale class on youtube, I covered this issue near the beginning of that class.

  • #141 Gunnar Tapper said

    @Miguel. Thanks, I now understand the imports. The routing still doesn't work so I'm missing something.

    testapp.py

    from flask import Flask
    from flask_restful import Api
    from api.v1 import api as api_v1
    
    app = Flask(__name__)
    
    app.register_blueprint(api_v1, url_prefix='/v1')
    
    if __name__ == '__main__':
       app.run(debug=True)
    

    ./api/v1/init.py:

    from flask import Blueprint
    
    api = Blueprint( 'api', __name__ )
    

    ./api/v1/routes.py:

    from flask import request
    from flask_restful import Resource
    
    tests = {}
    
    # api is defined in __init__.py
    # @api_v1.route('/test/<string:test_id>', methods = ['GET', 'PUT'])
    # def do_test( test_id ):
    #     if request.method == 'GET':
    #        return { test_id: tests[test_id] }
    
    #     if request.method == 'PUT':
    #        tests[test_id] = request.form['data']
    #        return { test_id: tests[test_id] }
    
    class TestAPI( Resource ):
       def get( self, test_id ):
           return { test_id: tests[test_id] }
    
       def put( self, test_id ):
           tests[test_id] = request.form['data']
           return { test_id: tests[test_id] }
    
    api.add_resource( TestAPI, '/test/<string:test_id>' )
    

    I tested both approaches (comment/uncomment as needed.)

    python testapp.py
    * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    * Restarting with stat
    * Debugger is active!
    * Debugger pin code: 151-213-751
    127.0.0.1 - - [30/Nov/2016 22:31:41] "PUT /v1/test/foo HTTP/1.1" 404 -

    Request:

    $ curl http://localhost:5000/v1/test/foo -d "data=2010-01-01,10,/foo/bar,20" -X PUT
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
    <title>404 Not Found</title>
    <h1>Not Found</h1>
    <p>The requested URL was not found on the server.  If you entered the URL manually please check your     spelling and try again.</p>
    

    I'm sorry if I'm missing something obvious here. Hopefully, this discussion will generate a straightforward skeleton once working.

  • #142 Miguel Grinberg said

    @Gunnar: Looks like you are not importing the routes, so they never get registered with the blueprint. Add a "from . import routes" right below the Blueprint creation line.

  • #143 Gunnar Tapper said

    @Miguel. As it turns out, the use of blueprint is a bit more involved. Here's a working example:

    testapp.py

    from flask import Flask
    from flask_restful import Api
    from api.v1 import api_bp as api_v1
    
    app = Flask(__name__)
    
    app.register_blueprint( api_v1, url_prefix='/v1' )
    
    if __name__ == '__main__':
       app.run(debug=True)
    

    NOTE: Import of blueprint "api_bp" as "api_v1". (This was named "api" in previous examples; "api_bp" seems clearer.)

    api/v1/init.py

    from flask import Blueprint
    from flask_restful import Api
    
    api_bp = Blueprint( 'api', __name__ )
    api = Api( api_bp )
    
    from routes import TestAPI
    api.add_resource( TestAPI, '/test/<string:test_id>' )
    

    NOTE: Import of class "TestAPI" from api/v1/routes.py. Addition of "api = Api( api_bp)", which is what's required for "api.add_resource".

    api/v1/routes.py:

    from flask import request
    from flask_restful import Resource
    
    tests = {}
    
    class TestAPI( Resource ):
       def get( self, test_id ):
           return { test_id: tests[test_id] }
    
       def put( self, test_id ):
           tests[test_id] = request.form['data']
           return { test_id: tests[test_id] }
    

    NOTE: I tried to include the routing information in "api/v1/routes.py" but that doesn't work.

    Test run:

    $ curl http://localhost:5000/v1/test/foo -d "data=Remember the milk" -X PUT
    {
        "foo": "Remember the milk"
    }
    
    $ curl http://localhost:5000/v1/test/foo
    {
        "foo": "Remember the milk"
    }
    
    $ python testapp.py
     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
     * Restarting with stat
     * Debugger is active!
     * Debugger pin code: 151-213-751
    127.0.0.1 - - [01/Dec/2016 22:21:59] "PUT /v1/test/foo HTTP/1.1" 200 -
    127.0.0.1 - - [01/Dec/2016 22:22:01] "GET /v1/test/foo HTTP/1.1" 200 -
    

    I hope this helps anyone who wants to use this cool versioning approach.

  • #144 Ditmr said

    Hi Miguel,

    thank you for your tutorials!

    I just started your O'Reilly Web API video course and ran into the first problem in Chapter "API Demonstrations":

    when I do:
    http GET http://localhost:5000/customers/

    I get:
    http: error: ConnectionError: HTTPConnectionPool(host='localhost', port=5000): Max retries exceeded with url: /customers/ (Caused by <class 'ConnectionRefusedError'>: [Errno 61] Connection refused)

    can you suggest any way forward?

    I have tried with python 3.6 as well as 3.4.4

    Should I expect problems with python 3.6.0?

    Thank you for your help.

    Cheers!

  • #145 Miguel Grinberg said

    @Ditmr: Did you start the Flask server before sending the request?

  • #146 Ditmr said

    Thank you Miguel,

    python api.py in second terminal did the trick!

  • #147 Altaf said

    Love the way you explain things, simple and straight forward tutorials.

  • #148 Lance contreras said

    Hi Miguel, Very nice article. I hope you can create an article that will discuss the details on how we can design the file structure, development and production strategy. I'm relatively new to python and I can simply follow good tutorial. But most tutorials deals on how to code python. But the workflow and file structure of how a good project should look like, that's what I'm really missing.

  • #149 Miguel Grinberg said

    @Lance: I have written and spoken extensively about Flask project structure. You may want to look at my Flask Web Development book, or the tutorials I gave at the last three PyCons, which are all available on YouTube.

  • #150 Mark Jeghers said

    Is it just me or is Flask-RESTful a lot slower than just Flask? I plugged my own data (with more deeply nested and large data) into both versions of the REST API and the Flask-RESTful version is WAY slower (5 sec). Regular Flask version is almost instantaneous response.

Leave a Comment