Building a Todo App with Flask in Python

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 PATCHHEAD, 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.

Creating Virtual Environment

python3 -m venv env

# Activate the virtual environment
source ./env/bin/activate

Setting up Flask

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.

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.

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"}

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!

Leave a Reply

Your email address will not be published. Required fields are marked *