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,

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)


Source code on GitHub: https://github.com/SoniArpit/simple-todoapp-fastapi
Happy Coding 🙂
You may also like 👇