Introduction
In this tutorial, we are going to build an API, or a web service, for a todo app. The API service will be implemented using a REST-based architecture.
Our app will have the following main features:
- Create an item in the todo list
- Read the complete todo list
- Update the items with status as "Not Started", "In Progress", or "Complete"
- Delete the items from the list
What is REST?
REST, or REpresentational State Transfer, is an architectural style for building web services and APIs. It requires the systems implementing REST to be stateless. The client sends a request to the server to retrieve or modify resources without knowing what state the server is in. The servers send the response to the client without needing to know what was the previous communication with the client.
Each request to the RESTful system commonly uses these 4 HTTP verbs:
- GET: Get a specific resource or a collection of resources
- POST: Create a new resource
- PUT: Update a specific resource
- DELETE: Remove a specific resource
Although others are permitted and sometimes used, like PATCH, HEAD, and OPTIONS.
What is Flask?
Flask is a framework for Python to develop web applications. It is non-opinionated, meaning that it does not make decisions for you. Because of this, it does not restrict to structure your application in a particular way. It provides greater flexibility and control to developers using it. Flask provides you with the base tools to create a web app, and it can be easily extended to include most things that you would need to include in your app.
Some other popular web frameworks can be considered as an alternative to Flask. Django is one of the most popular alternatives if Flask doesn't work for you. We have done a comparison between Django and Flask in this tutorial.
Setting up Flask
First, let's go ahead and install Flask using pip:
$ pip install Flask
Let us quickly configure Flask and spin up a web server in our local machine. Create a file main.py
in the todo_service_flask
directory:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
After importing Flask, we set up a route. A route is specified by a URL pattern, an HTTP method, and a function which receives and handles an HTTP request. We've bound that route with a Python function that will be invoked every time that URL is requested via HTTP. In this case, we've set up the root (/) route so that it can be accessed by the URL pattern http://[IP-OR-DOMAIN]:[PORT]/
.
Running the Flask app
The next job is to spin up a local server and serve this web service so that we can access it through a client.
Thankfully, this can all be done with a single, simple command:
$ FLASK_APP=main.py flask run
You should see the message in the console:
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
We can use cURL to fire a GET request. If you are on Mac, cURL should already be installed in your system:
$ curl -X GET http://127.0.0.1:5000/
We should be greeted with the response:
Hello World!
The story doesn't end here. Let's go ahead and structure our Todo application.
Structuring the Todo App
Our Todo app will have several fundamental features:
- Adding items to a list
- Getting all items from the list
- Updating an item in the list
- Deleting an item from the list
These are often referred to as CRUD operations, for create, read, update, and delete.
We'll use the SQLite database to store data, which is a very lightweight file-based database. You can install the DB Browser for SQLite to easily create a database.
Let's name this database todo.db
and place it under the directory todo_service_flask
. Now, to create a table, we run a simply query:
CREATE TABLE "items" (
"item" TEXT NOT NULL,
"status" TEXT NOT NULL,
PRIMARY KEY("item")
);
Also, to keep things simple we'll write all of our routes in a single file, though this isn't always a good practice, especially for very large apps.
We'll also use one more file to contain our helper functions. These functions will have the business logic to process the request by connecting to the database and executing the appropriate queries.
Once you are comfortable with this initial Flask structure you can restructure your app any way you like.
Building the App
To avoid writing logic multiple times for tasks that are commonly executed, such as adding items to a database, we can define helper functions in a separate file and simply call them when need be. For this tutorial we'll name the file helper.py
.
Adding Items
To implement this feature we need two things:
- A helper function that contains business logic to add a new element in the database
- A route that should be called whenever a particular HTTP endpoint is hit
First, let's define some constants and write the add_to_list()
function:
import sqlite3
DB_PATH = './todo.db' # Update this path accordingly
NOTSTARTED = 'Not Started'
INPROGRESS = 'In Progress'
COMPLETED = 'Completed'
def add_to_list(item):
try:
conn = sqlite3.connect(DB_PATH)
# Once a connection has been established, we use the cursor
# object to execute queries
c = conn.cursor()
# Keep the initial status as Not Started
c.execute('insert into items(item, status) values(?,?)', (item, NOTSTARTED))
# We commit to save the change
conn.commit()
return {"item": item, "status": NOTSTARTED}
except Exception as e:
print('Error: ', e)
return None
This function establishes a connection with the database and executes an insert query. It returns the inserted item and its status.
Next, we'll import some modules and set up a route for the path /item/new
:
import helper
from flask import Flask, request, Response
import json
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
@app.route('/item/new', methods=['POST'])
def add_item():
# Get item from the POST body
req_data = request.get_json()
item = req_data['item']
# Add item to the list
res_data = helper.add_to_list(item)
# Return error if item not added
if res_data is None:
response = Response("{'error': 'Item not added - " + item + "'}", status=400 , mimetype='application/json')
return response
# Return response
response = Response(json.dumps(res_data), mimetype='application/json')
return response
The request
module is used to parse the request and get HTTP body data or the query parameters from the URL. response
is used to return a response to the client. The response is of type JSON.
If you'd like to read more about Reading and Writing JSON in Python, we've got you covered!
We returned a status of 400 if the item was not added due to some client error. The json.dumps()
function converts the Python object or dictionary into a valid JSON object.
Let us save the code and verify if our feature is implemented correctly.
We can use cURL to send a POST request and test out our app. We also need to pass the item name as the POST body:
$ curl -X POST http://127.0.0.1:5000/item -d '{"item": "Setting up Flask"}' -H 'Content-Type: application/json'
If you are on Windows, you'll need to format the JSON data from single quotes to double quotes and escape it:
$ curl -X POST http://127.0.0.1:5000/item -d "{\"item\": \"Setting up Flask\"}" -H 'Content-Type: application/json'
Please note the following:
- Our URL consists of two parts - a base URL (http://127.0.0.1:5000) and the route or path (
/item/new
) - The request method is POST
- Once the request hits the web server, it tries to locate the endpoint based on this information
- We're passing the data in JSON format - {"item": "Setting up Flask"}
As we fire the request we should be greeted with the response:
{"Setting up Flask": "Not Started"}
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Let us run the following command to add one more item to the list:
$ curl -X POST http://127.0.0.1:5000/item -d '{"item": "Implement POST endpoint"}' -H 'Content-Type: application/json'
We should be greeted with the response, which shows us the task description and its status:
{"Implement POST endpoint": "Not Started"}
Congratulations!!! We've successfully implemented the functionality to add an item to the todo list.
Retrieving All Items
We often wish to get all items from a list, which is thankfully very easy:
def get_all_items():
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('select * from items')
rows = c.fetchall()
return { "count": len(rows), "items": rows }
except Exception as e:
print('Error: ', e)
return None
This function establishes a connection with the database and creates a SELECT query and then executes it via c.fetchall()
. This returns all records returned by the SELECT query. If we are interested in only one item we can instead call c.fetchone()
.
Our method, get_all_items
returns a Python object containing 2 items:
- The number of items returned by this query
- The actual items returned by the query
In main.py
, we'll define a route /item/new
that accepts a GET request. Here we won't pass the methods
keyword argument to @app.route()
, because if we skip this parameter then it is defaulted to GET:
@app.route('/items/all')
def get_all_items():
# Get items from the helper
res_data = helper.get_all_items()
# Return response
response = Response(json.dumps(res_data), mimetype='application/json')
return response
Let's use cURL to fetch the items and test our route:
$ curl -X GET http://127.0.0.1:5000/items/all
We should be greeted with the response:
json {"count": 2, "items": [["Setting up Flask", "Not Started"], [Implement POST endpoint", "Not Started"]]}
Getting Status of Individual Items
Like we did with the previous example, we'll write a helper function for this:
def get_item(item):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("select status from items where item='%s'" % item)
status = c.fetchone()[0]
return status
except Exception as e:
print('Error: ', e)
return None
We'll also define a route in main.py
to parse the request and serve the response. We need the route to accept a GET request and the item name should be submitted as a query parameter.
A query parameter is passed in the format ?name=value
with the URL. e.g. http://base-url/path/to/resource/?name=value
. If there are spaces in the value you need to replace them with either +
or with %20
, which is the URL-encoded version of a space. You can have multiple name-value pairs by separating them with the &
character.
Here are some of the valid examples of query parameters:
http://127.0.0.1:8080/search?query=what+is+flask
http://127.0.0.1:8080/search?category=mobiles&brand=apple
@app.route('/item/status', methods=['GET'])
def get_item():
# Get parameter from the URL
item_name = request.args.get('name')
# Get items from the helper
status = helper.get_item(item_name)
# Return 404 if item not found
if status is None:
response = Response("{'error': 'Item Not Found - %s'}" % item_name, status=404 , mimetype='application/json')
return response
# Return status
res_data = {
'status': status
}
response = Response(json.dumps(res_data), status=200, mimetype='application/json')
return response
Again, let's use cURL to fire the request:
$ curl -X GET http://127.0.0.1:5000/item/status?name=Setting+up+Flask
We should be greeted with the response:
{"status": "Not Started"}
Updating Items
Since we have completed the task "Setting up Flask" a while ago, it's high time we should update its status to "Completed".
First, let's write a function in helper.py
that executes the update query:
def update_status(item, status):
# Check if the passed status is a valid value
if (status.lower().strip() == 'not started'):
status = NOTSTARTED
elif (status.lower().strip() == 'in progress'):
status = INPROGRESS
elif (status.lower().strip() == 'completed'):
status = COMPLETED
else:
print("Invalid Status: " + status)
return None
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('update items set status=? where item=?', (status, item))
conn.commit()
return {item: status}
except Exception as e:
print('Error: ', e)
return None
It is good practice not to rely on user input and do our validations, as we never know what the end-user might do with our application. Very simple validations are done here, but if this were a real-world application then we'd want to protect against other malicious input, like SQL injection attacks.
Next, we'll setup a route in main.py
that accepts a PUT method to update the resource:
@app.route('/item/update', methods=['PUT'])
def update_status():
# Get item from the POST body
req_data = request.get_json()
item = req_data['item']
status = req_data['status']
# Update item in the list
res_data = helper.update_status(item, status)
# Return error if the status could not be updated
if res_data is None:
response = Response("{'error': 'Error updating item - '" + item + ", " + status + "}", status=400 , mimetype='application/json')
return response
# Return response
response = Response(json.dumps(res_data), mimetype='application/json')
return response
Let's use cURL to test this route, just as before:
$ curl -X PUT http://127.0.0.1:5000/item/update -d '{"item": "Setting up Flask", "status": "Completed"}' -H 'Content-Type: application/json'
We should be greeted with the response:
{"Setting up Flask": "Completed"}
Deleting Items
First, we'll write a function in helper.py
that executes the delete query:
def delete_item(item):
try:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('delete from items where item=?', (item,))
conn.commit()
return {'item': item}
except Exception as e:
print('Error: ', e)
return None
Note: Please note that (item,)
is not a typo. We need to pass execute()
a tuple even if there is only one item in the tuple. Adding the comma forces this to become a tuple.
Next, we'll setup a route in main.py
that accepts the DELETE request:
@app.route('/item/remove', methods=['DELETE'])
def delete_item():
# Get item from the POST body
req_data = request.get_json()
item = req_data['item']
# Delete item from the list
res_data = helper.delete_item(item)
# Return error if the item could not be deleted
if res_data is None:
response = Response("{'error': 'Error deleting item - '" + item + "}", status=400 , mimetype='application/json')
return response
# Return response
response = Response(json.dumps(res_data), mimetype='application/json')
return response
Let's use cURL to test our delete route:
$ curl -X DELETE http://127.0.0.1:5000/item/remove -d '{"item": "Setting up Flask"}' -H 'Content-Type: application/json'
We should be greeted with the response:
{"item": "Temporary item to be deleted"}
And that rounds up the app with all the back-end features we need!
Conclusion
I hope this tutorial gave you a good understanding of how to use Flask to build a simple REST-based web application. If you have experience with other Python frameworks like Django, you may have observed it to be much easier to use Flask.
This tutorial focused more on the back-end aspect of the application, without any GUI, though you can also use Flask to render HTML pages and templates, which we'll save for another article.
While it is perfectly fine to use Flask to manage HTML templates, most people use Flask to build backend services and build the frontend part of the app by using any of the popular JavaScript libraries. You can try what works for you best. Good luck on your Flask journey!
If you'd like to play around with the source code or have any difficulties running it from the code above, here it is on GitHub!