📜 ⬆️ ⬇️

Designing a RESTful API with Python and Flask

In recent years, REST (REpresentational State Transfer) has become a standard architecture in the design of web services and web APIs.

In this article, I will show you how easy it is to create RESTful web services using Python and the Flask microframe.

What is REST?


The characteristics of the REST system are defined by the six design rules:
')


What is a RESTful web service?


The REST architecture is designed to conform to the HTTP protocol used on the Internet.


Central to the concept of RESTful web services is the concept of resources. Resources are represented by URIs . Clients send requests to these URIs using methods provided by the HTTP protocol, and possibly change the state of these resources.


HTTP methods are designed to affect a resource in a standard way:


HTTP methodActExample
GetGet resource informationexample.com/api/orders
(get a list of orders)
GetGet resource informationexample.com/api/orders/123
(get order # 123)
POSTCreate a new resourceexample.com/api/orders
(create a new order from the data sent with the request)
PUTUpdate resourceexample.com/api/orders/123
(update order # 123 data provided with request)
DELETEDelete resourceexample.com/api/orders/123
(delete order # 123)


The REST design does not recommend what specifically the format of the data sent with requests should be. The data passed in the request body can be a JSON blob, or by using arguments in the URL.



We design a simple web service.


When designing a web service or API, you need to determine the resources that will be available and the requests that will use this data according to the REST rules.


Suppose we want to write a To Do List application and we need to design a web service for it. The first thing we need to do is to come up with a root URL to access this service. For example, we could come up with something like the root URL:


http://[hostname]/todo/api/v1.0/ 

Here I decided to include the application name and API version in the URL. Adding the application name to the URL is a good way to share the services running on the same server. Adding a version of the API to the URL can help if you want to upgrade in the future and introduce incompatible features in the new version and don’t want to break the running applications that run on the old API.


The next step is to select the resources that will be available through our service. We have a very simple application, we have only tasks, so our resources can only be tasks from our ToDo sheet.


To access resources, we will use the following HTTP methods:


HTTP methodURIAct
Gethttp: // [hostname] /todo/api/v1.0/tasksGet task list
Gethttp: // [hostname] /todo/api/v1.0/tasks/ [task_id]Get task
POSThttp: // [hostname] /todo/api/v1.0/tasksCreate a new task
PUThttp: // [hostname] /todo/api/v1.0/tasks/ [task_id]Update existing task
DELETEhttp: // [hostname] /todo/api/v1.0/tasks/ [task_id]Delete task


Our task will have the following fields:




This concludes the part dedicated to the design of our service. It remains only to realize it!



A brief introduction to the Flask microframe


If you read the Flask Mega-Tutorial series, you know that Flask is a simple and powerful enough Python web framework.


Before we dive into the specifics of web services, let's look at how Flask applications are usually implemented.


I assume that you are familiar with the basics of working with Python on your platform. In the examples, I will use a Unix-like operating system. In short, it means that they will work on Linux, MacOS X and even on Windows if you use Cygwin . The commands will be slightly different if you use the native version of Python for Windows.



First, install Flask in a virtual environment. If virtualenv not installed on your system, you can download it from https://pypi.python.org/pypi/virtualenv .


 $ mkdir todo-api $ cd todo-api $ virtualenv flask New python executable in flask/bin/python Installing setuptools............................done. Installing pip...................done. $ flask/bin/pip install flask 


Now that the Flask is installed, let's create a simple web application, to do this, put the following code in app.py :


 #!flask/bin/python from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "Hello, World!" if __name__ == '__main__': app.run(debug=True) 


To start the application, we need to start app.py :


 $ chmod a+x app.py $ ./app.py * Running on http://127.0.0.1:5000/ * Restarting with reloader 


Now you can launch a web browser from type http://localhost:5000 to see our little application in action.


Simple, isn't it? Now we will convert our application to a RESTful service!



Implementing a RESTful Python and Flask service


Creating a web service on Flask is surprisingly simple, much easier than building full-fledged server applications, like the one we did in the Mega-Tutorial series .


There are a couple of good extensions for Flask that can facilitate the creation of RESTful services, but our task is so simple that using extensions is unnecessary.


Clients of our web service will ask the service to add, delete and modify tasks, so we need an easy way to store tasks. The obvious way to do this is to make a small database, but since the database goes beyond the topic of the article, we will do everything much easier. To learn more about the correct use of the database with Flask, I recommend again reading my Mega-Tutorial .



Instead of a database, we will keep a list of our tasks in memory. This will work only if we work with the server in one thread and in one process. Although this is normal for a development server, for a production server this would be a very bad idea and it would be better to think about using a database.


Now we are ready to implement the first entry point to our web service:


 #!flask/bin/python from flask import Flask, jsonify app = Flask(__name__) tasks = [ { 'id': 1, 'title': u'Buy groceries', 'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 'done': False }, { 'id': 2, 'title': u'Learn Python', 'description': u'Need to find a good Python tutorial on the web', 'done': False } ] @app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': tasks}) if __name__ == '__main__': app.run(debug=True) 


As you can see, little has changed. We created tasks in memory that are nothing more than a simple array of dictionaries. Each record in the array has all the fields that we defined above for our tasks.


Instead of using the index entry point, we now have a get_tasks function associated with the URI /todo/api/v1.0/tasks , for the HTTP GET method.


Instead of text, our function sends JSON, into which Flask encodes our data structure using the jsonify method.


Using a web browser to test a web service is not a good idea, because Using a web browser is not so easy to generate all types of HTTP requests. Instead, we will use curl . If you don't have curl installed, it’s best to do it right now.


Start the web service in the same way as the demo application by running app.py Now open a new console window and enter the following commands:


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 294 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 04:53:53 GMT { "tasks": [ { "description": "Milk, Cheese, Pizza, Fruit, Tylenol", "done": false, "id": 1, "title": "Buy groceries" }, { "description": "Need to find a good Python tutorial on the web", "done": false, "id": 2, "title": "Learn Python" } ] } 


We just called the function of our RESTful service!



Now let's write the second version of the GET method for our tasks. If you look at the table above, the following will be the method that returns data from one task:


 from flask import abort @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET']) def get_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) return jsonify({'task': task[0]}) 


The second function is a bit more interesting. Here we pass the task id through the URL, and with the help of Flask we translate the task_id function task_id .


With this argument we are looking for our task in the database. If the received id is not found in the database, we will return a 404 error, which according to the HTTP specification means “Resource Not Found”.


If the task is found, we simply pack it in JSON using the jsonify function and send it as an answer, just as we did before sending the collection.


This is what the action of this function looks like when we call it with curl :


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 151 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:21:50 GMT { "task": { "description": "Need to find a good Python tutorial on the web", "done": false, "id": 2, "title": "Learn Python" } } $ curl -i http://localhost:5000/todo/api/v1.0/tasks/3 HTTP/1.0 404 NOT FOUND Content-Type: text/html Content-Length: 238 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:21:52 GMT <!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.</p><p>If you entered the URL manually please check your spelling and try again.</p> 


When we requested a resource with id # 2 we received it, but instead of a resource with id # 3 we received an error 404. We received such a strange error inside HTML instead of JSON, because Flask by default generates a page with error 404. Since this is a client applications will always wait for our JSON server, then we need to change this behavior:


 from flask import make_response @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404) 


So we will get a response more appropriate to our API:


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks/3 HTTP/1.0 404 NOT FOUND Content-Type: application/json Content-Length: 26 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:36:54 GMT { "error": "Not found" } 


The following POST method in our list will be used to add a new task to our database:


 from flask import request @app.route('/todo/api/v1.0/tasks', methods=['POST']) def create_task(): if not request.json or not 'title' in request.json: abort(400) task = { 'id': tasks[-1]['id'] + 1, 'title': request.json['title'], 'description': request.json.get('description', ""), 'done': False } tasks.append(task) return jsonify({'task': task}), 201 


Adding a new task is also quite simple. request.json contains the request data, but only if it is marked as JSON. If there is no data, or the data is in place but the value of the title field is missing, then the code 400 is returned, which is used to denote “Bad Request”.


Then we create a dictionary with a new task, using the id of the last task plus 1 (a simple way to ensure that the id is unique in our simple database). We tolerate the absence of a value in the description field, and we assume that the done field when creating a task will always be False .



We add a new task to our tasks array, then return the saved task and code 201 to the client, which in HTTP means “Created”.


To test a new feature, we use the following curl command:


 $ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 201 Created Content-Type: application/json Content-Length: 104 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:56:21 GMT { "task": { "description": "", "done": false, "id": 3, "title": "Read a book" } } 


Note: if you have Windows and you are using the Cygwin curl version of bash then the above command will work as expected. If you are using the native version of curl from the usual command line, you'll have to podshamanit a little with double quotes:


 curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks 


In Windows, you use double quotes to separate the body of the request, and inside the request, double quotes to escape the third quote.


Of course, after completing this request, we can get an updated list of tasks:


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 423 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 05:57:44 GMT { "tasks": [ { "description": "Milk, Cheese, Pizza, Fruit, Tylenol", "done": false, "id": 1, "title": "Buy groceries" }, { "description": "Need to find a good Python tutorial on the web", "done": false, "id": 2, "title": "Learn Python" }, { "description": "", "done": false, "id": 3, "title": "Read a book" } ] } 


The remaining two functions of our web service will look like this:


 @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT']) 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': task[0]}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE']) def delete_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) tasks.remove(task[0]) return jsonify({'result': True}) 


The delete_task function without surprises. For the update_task function update_task we try to prevent errors by carefully checking the input arguments. We must make sure that the data provided by the client is in the proper format before writing it into the database.



The call to the task updating function with id # 2 will look like this:


 $ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 170 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 07:10:16 GMT { "task": [ { "description": "Need to find a good Python tutorial on the web", "done": true, "id": 2, "title": "Learn Python" } ] } 


We improve the interface of our service


Now the main problem with the design of our service is that customers are forced to build URIs independently based on task IDs. This one is easy, but it gives the client knowledge of how URIs are built to access the data, which can interfere in the future if we want to make changes to the URI.


Instead of the task id, we will return the full URI through which all actions with the task will be performed. To do this, we will write a small helper function that will generate a “public” version of the task sent to the client:


 from flask import url_for def make_public_task(task): new_task = {} for field in task: if field == 'id': new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True) else: new_task[field] = task[field] return new_task 


All we do here is take a task from our database and create a new task in which all fields are identical, except for the id field, which is replaced by the uri field generated by the url_for function provided by Flask.


When we return the task list, we run all tasks through this function before sending to the client:


 @app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': map(make_public_task, tasks)}) 


Now the client receives this list of tasks:


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 406 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 18:16:28 GMT { "tasks": [ { "title": "Buy groceries", "done": false, "description": "Milk, Cheese, Pizza, Fruit, Tylenol", "uri": "http://localhost:5000/todo/api/v1.0/tasks/1" }, { "title": "Learn Python", "done": false, "description": "Need to find a good Python tutorial on the web", "uri": "http://localhost:5000/todo/api/v1.0/tasks/2" } ] } 


By applying this technique to the rest of the functions, we can ensure that the client always receives a URI instead of id.


Protect RESTful web service


Did you think we already finished? Of course, we are finished with the functionality of our service, but we have a problem. Our service is open to all, and this is not very good.


We have a complete web service that manages our to-do list, but the service, in its current state, is available to everyone. If a stranger finds out how our API works, he or she can write a new client and make a mess of our data.


Many beginner guides ignore security and end here. In my opinion this is a serious problem that must always be resolved.


The simplest way to protect our web service is to let customers in after login and password authorization. In a regular web application, you must create a login form that sends authorization data, the server processes it and makes a new session, and the user's browser receives a cookie with a session identifier. Unfortunately, we cannot do this here, stateless is one of the rules for constructing REST web services and we must ask customers to send their registration data with each request.


With REST, we always try to adhere to the HTTP protocol as much as we can. Now we need to implement user authentication in the context of HTTP, which provides us with 2 options - Basic and Digest .



There is a small Flask extension written by your humble servant. Let's install Flask-HTTPAuth :


 $ flask/bin/pip install flask-httpauth 


Now let's tell our web service to give data only to the user with the login miguel and the password python . First, let's configure Basic HTTP authentication as shown below:


 from flask.ext.httpauth import HTTPBasicAuth auth = HTTPBasicAuth() @auth.get_password def get_password(username): if username == 'miguel': return 'python' return None @auth.error_handler def unauthorized(): return make_response(jsonify({'error': 'Unauthorized access'}), 401) 


The get_password function will return a password by username. In more complex systems, such a function will have to climb into the database, but for one user it is not necessary.


The error_handler function will be used to send an authorization error with incorrect data. Just like we did with other errors, we need to configure the function to send JSON, instead of HTML.



After setting up the authentication system, all that remains is to add the @auth.login_required decorator for all functions that need to be protected. For example:


 @app.route('/todo/api/v1.0/tasks', methods=['GET']) @auth.login_required def get_tasks(): return jsonify({'tasks': tasks}) 


If we try to request this function using curl we get something like this:


 $ curl -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 401 UNAUTHORIZED Content-Type: application/json Content-Length: 36 WWW-Authenticate: Basic realm="Authentication Required" Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 06:41:14 GMT { "error": "Unauthorized access" } 


In order to call this feature, we must confirm our credentials:


 $ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks HTTP/1.0 200 OK Content-Type: application/json Content-Length: 316 Server: Werkzeug/0.8.3 Python/2.7.3 Date: Mon, 20 May 2013 06:46:45 GMT { "tasks": [ { "title": "Buy groceries", "done": false, "description": "Milk, Cheese, Pizza, Fruit, Tylenol", "uri": "http://localhost:5000/todo/api/v1.0/tasks/1" }, { "title": "Learn Python", "done": false, "description": "Need to find a good Python tutorial on the web", "uri": "http://localhost:5000/todo/api/v1.0/tasks/2" } ] } 


An authentication extension gives us the freedom to choose which functions will be shared and which ones will be protected.


To protect registration information, our web service must be accessible via HTTP Secure server (...) which encrypts traffic between the client and the server and prevents third-party receipt of confidential information.


Unfortunately, web browsers have a bad habit of showing a scary dialog when the request returns with an error of 401. This happens even for background requests, so if we implemented the client for the web browser, we would have to jump through hoops to prevent the browser show your windows


The simple way to deceive the browser is to return any other code, instead of 401. The favorite alternative of all is 403, which means “Forbidden” error. Although it is quite close to the meaning of the error, it violates the standard HTTP, so it is wrong. In particular, it would be a good decision not to use a web browser as a client application. But in cases where the server and the client are developed together, it saves from many troubles. To crank this trick we just need to replace the error code from 401 to 403:


 @auth.error_handler def unauthorized(): return make_response(jsonify({'error': 'Unauthorized access'}), 403) 


In the client application, you also need to catch error 403.


Possible improvements


There are several ways to improve the web service we developed today.


For starters, a real web service must communicate with a real database. The data structure in memory is a very limited way of storing data and should not be used in real-world applications.


Another way to improve the application is to support multiple users. If the system supports multiple users, authentication data can be used to return personal lists to users. In such a system, users will become the second resource. POST request will register a new user in the system. A GET may return user information. PUT request can update user information, such as email. A DELETE request will delete the user from the system.


A GET request that returns a list of tasks can be extended in several ways. For starters, this query can have optional arguments, such as the number of tasks per page. Another way to make the function more convenient is to add filtering criteria. For example, a client can request only completed tasks or tasks that start with a specific letter. All these elements can be added to the URL as arguments.


Conclusion


You can get the finished code for the To Do List web service here: https://gist.github.com/miguelgrinberg/5614326 .


I believe this was a simple and friendly introduction to the RESTful API. If there is enough interest, I could write the second part of this article, in which we will develop a simple web client for our service.


I made a client for our service: Writing a Javascript REST client .


An article about the same server, but using Flask-RESTful Designing a RESTful API using Flask-RESTful .


Miguel

Source: https://habr.com/ru/post/246699/


All Articles