Designing a RESTful API using Flask-RESTful
Posted by
on underThis 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 Method | URI | Action |
---|---|---|
GET | http://[hostname]/todo/api/v1.0/tasks | Retrieve list of tasks |
GET | http://[hostname]/todo/api/v1.0/tasks/[task_id] | Retrieve a task |
POST | http://[hostname]/todo/api/v1.0/tasks | Create a new task |
PUT | http://[hostname]/todo/api/v1.0/tasks/[task_id] | Update an existing task |
DELETE | http://[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
-
#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 TaskAPIapi.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 Resourcemetrics = {}
<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.
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:
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.