Simple ToDo App in FastAPI with Jinja2 Template

Hey internet programmers, recently I was trying FastAPI (FastAPI is a Web framework for developing RESTful APIs in Python) for my next project and it is really amazing. So let me share a small tutorial on making a ToDo app using FastAPI and with the Jinja2 template.

Create Virtual Environment

First, create a directory name it todoapp and open terminal, and cd into todoapp then you need to type the following command.

virtualenv env

Assuming virtualenv is already installed on your PC. If not please visit this -> https://virtualenv.pypa.io/en/latest/installation.html

Now activate the virtual environment

source env/bin/activate

Installation

Install FastAPI. make sure your environment is activated

pip install fastapi

Also, install uvicorn to work as the server

pip install "uvicorn[standard]"

Install package for templating

pip install jinja2 python-multipart

Install package for database support

pip install sqlalchemy

Now make main.py, database.py, and model.py in the same directory (in todoapp directory) and also make the templates and static directory, and its look like this,

fastapi project structure

do not worry about __pycache__

main.py

# main.py

from fastapi import FastAPI, Request, Depends, Form, status
from fastapi.templating import Jinja2Templates
import models
from database import engine, sessionlocal
from sqlalchemy.orm import Session
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles

models.Base.metadata.create_all(bind=engine)

templates = Jinja2Templates(directory="templates")

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

def get_db():
    db = sessionlocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/")
async def home(request: Request, db: Session = Depends(get_db)):
    todos = db.query(models.Todo).order_by(models.Todo.id.desc())
    return templates.TemplateResponse("index.html", {"request": request, "todos": todos})

@app.post("/add")
async def add(request: Request, task: str = Form(...), db: Session = Depends(get_db)):
    todo = models.Todo(task=task)
    db.add(todo)
    db.commit()
    return RedirectResponse(url=app.url_path_for("home"), status_code=status.HTTP_303_SEE_OTHER)

@app.get("/edit/{todo_id}")
async def add(request: Request, todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    return templates.TemplateResponse("edit.html", {"request": request, "todo": todo})

@app.post("/edit/{todo_id}")
async def add(request: Request, todo_id: int, task: str = Form(...), completed: bool = Form(False), db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    todo.task = task
    todo.completed = completed
    db.commit()
    return RedirectResponse(url=app.url_path_for("home"), status_code=status.HTTP_303_SEE_OTHER)

@app.get("/delete/{todo_id}")
async def add(request: Request, todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
    db.delete(todo)
    db.commit()
    return RedirectResponse(url=app.url_path_for("home"), status_code=status.HTTP_303_SEE_OTHER)

database.py

# database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

DB_URL = 'sqlite:///todo.sqlite3'

engine = create_engine(DB_URL, connect_args={'check_same_thread': False})
sessionlocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

models.py

# models.py

from sqlalchemy import Column, Integer, Boolean, Text
from database import Base

class Todo(Base):
    __tablename__ = 'todos'
    id = Column(Integer, primary_key=True)
    task = Column(Text)
    completed = Column(Boolean, default=False)

    def __repr__(self):
        return '<Todo %r>' % (self.id)

Now let’s make templates inside the templates directory

templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ToDo App</title>
    <link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}">
</head>
<body>
    <main>
        <h1>ToDo App</h1>
        <br>
        {% block content %}
        
        {% endblock content %}
    </main>
</body>
</html>

templates/index.html

{% extends 'base.html' %}

{% block content %}

    <form action="/add" method="post">
        <textarea name="task" rows="5" placeholder="Enter your task"></textarea>
        <button type="submit">Add</button>
    </form>

    <br>
    <h2>Tasks</h2>

    <div>
        {% for todo in todos %}
            <div class="task">
                {% if todo.completed %}
                    <strike>{{ todo.task }}</strike>
                {% else %}
                    {{ todo.task }}
                {% endif %}
                <small>
                    <a href="edit/{{ todo.id }}">Edit</a>
                    <a href="delete/{{ todo.id }}">Delete</a>
                </small>
            </div>
        {% endfor %}
    </div>

{% endblock content %}

templates/edit.html

{% extends 'base.html' %}

{% block content %}
    <form action="/edit/{{todo.id}}" method="post">
        <textarea name="task" rows="5">{{todo.task}}</textarea>
        <label for="completed">Done?</label>
        <input type="checkbox" name="completed" {% if todo.completed %}checked{% endif %}>

        <button type="submit">Save</button>
    </form>
{% endblock content %}

static/css/main.css

let’s add some styling

*{
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

body{
    font-family: 'Roboto', sans-serif;
    background: #f5f5f5;
}

main{
    width: 100%;
    max-width: 520px;
    margin: 0 auto;
    padding: 0 20px;
}

textarea{
    width: 100%;
    height: 100px;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
    resize: none;
}

button{
    width: 100%;
    height: 40px;
    border: 1px solid #ccc;
    border-radius: 5px;
    background-color: #eee;
    color: #333;
    font-size: 16px;
    cursor: pointer;
}

.task{
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 0;
    border-bottom: 1px solid #ccc;
}

.task:first-child{
    border-top: 1px solid #ccc;
}

Output

To run the FastAPI app type the following command and hit enter

uvicorn main:app --reload

with uvicorn using the file_name:app_instance

open the link on the browser (http://127.0.0.1:8000)

todo app in fastapi home page

home page

todo app in fastapi edit page

edit page

Source code on GitHub: https://github.com/SoniArpit/simple-todoapp-fastapi

Happy Coding :)

You may also like 👇