A minimal clone of a Todo service using FastAPI and SQLite
Welcome to another official UVF IT guide where we roll up our sleeves and build a tiny yet functional Todo API, inspired by the FastAPI project. In just ~45 minutes you’ll have a local server that lets you create, read, update, and delete tasks—all backed by a lightweight SQLite database.
We’ll stay strictly on the free‑tier stack: Python 3.11, FastAPI, Uvicorn, and the built‑in sqlite3 module. No external paid services, no Docker, just pure code you can run on any laptop.
pip available – older versions miss type‑hint features used by FastAPI.First we create an isolated Python environment so our dependencies don’t clash with other projects. Then we install FastAPI, which gives us the web framework, and Uvicorn, the ASGI server that will run our app locally.
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install fastapi uvicornWe need a tiny SQLite helper that creates the todos table on first run and offers CRUD functions used later by the API routes. Keeping this logic in its own module (db.py) keeps main.py clean and testable.
db.py
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).with_name('todos.db')
def _get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
with _get_conn() as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
)
''')
conn.commit()
def create_todo(title: str):
with _get_conn() as conn:
cur = conn.execute('INSERT INTO todos (title) VALUES (?)', (title,))
conn.commit()
return cur.lastrowid
def get_todo(todo_id: int):
with _get_conn() as conn:
cur = conn.execute('SELECT * FROM todos WHERE id = ?', (todo_id,))
row = cur.fetchone()
return dict(row) if row else None
def list_todos():
with _get_conn() as conn:
cur = conn.execute('SELECT * FROM todos')
return [dict(r) for r in cur.fetchall()]
def update_todo(todo_id: int, title: str = None, completed: bool = None):
fields = []
params = []
if title is not None:
fields.append('title = ?')
params.append(title)
if completed is not None:
fields.append('completed = ?')
params.append(int(completed))
params.append(todo_id)
if not fields:
return False
with _get_conn() as conn:
conn.execute(f'UPDATE todos SET {', '.join(fields)} WHERE id = ?', params)
conn.commit()
return True
def delete_todo(todo_id: int):
with _get_conn() as conn:
conn.execute('DELETE FROM todos WHERE id = ?', (todo_id,))
conn.commit()
return True
# Initialise on import so the table exists before any route uses it
init_db()In this step we expose the classic CRUD operations for a Todo item as JSON endpoints. FastAPI will automatically generate OpenAPI documentation, so you’ll get a Swagger UI at /docs without extra work. Each route talks to the SQLite helper we created earlier, keeping the API thin and focused on request handling.
main.py
import sqlite3
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
DB = 'todos.db'
class Todo(BaseModel):
title: str
description: str | None = None
completed: bool = False
def get_db():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
@app.post('/todos', response_model=dict)
def create_todo(todo: Todo):
conn = get_db()
cur = conn.cursor()
cur.execute('INSERT INTO todos (title, description, completed) VALUES (?,?,?)',
(todo.title, todo.description, int(todo.completed)))
conn.commit()
todo_id = cur.lastrowid
conn.close()
return {'id': todo_id, **todo.dict()}
@app.get('/todos', response_model=list)
def list_todos():
conn = get_db()
rows = conn.execute('SELECT * FROM todos').fetchall()
conn.close()
return [dict(row) for row in rows]
@app.get('/todos/{todo_id}', response_model=dict)
def get_todo(todo_id: int):
conn = get_db()
row = conn.execute('SELECT * FROM todos WHERE id = ?', (todo_id,)).fetchone()
conn.close()
if row is None:
raise HTTPException(status_code=404, detail='Todo not found')
return dict(row)
@app.put('/todos/{todo_id}', response_model=dict)
def update_todo(todo_id: int, todo: Todo):
conn = get_db()
cur = conn.cursor()
cur.execute('UPDATE todos SET title = ?, description = ?, completed = ? WHERE id = ?',
(todo.title, todo.description, int(todo.completed), todo_id))
conn.commit()
if cur.rowcount == 0:
conn.close()
raise HTTPException(status_code=404, detail='Todo not found')
conn.close()
return {'id': todo_id, **todo.dict()}
@app.delete('/todos/{todo_id}', response_model=dict)
def delete_todo(todo_id: int):
conn = get_db()
cur = conn.cursor()
cur.execute('DELETE FROM todos WHERE id = ?', (todo_id,))
conn.commit()
if cur.rowcount == 0:
conn.close()
raise HTTPException(status_code=404, detail='Todo not found')
conn.close()
return {'detail': f'Todo {todo_id} deleted'}Now that the API is defined, start the development server with Uvicorn. The --reload flag watches your files and restarts automatically, which is handy while you iterate. Once the server is up, you can visit the auto‑generated docs at http://127.0.0.1:8000/docs to explore the endpoints visually.
uvicorn main:app --reloadA quick curl sequence proves the CRUD flow works: create a todo, list all todos, and fetch the newly created one. After confirming the JSON responses, open the Swagger UI to try the same operations with a friendly UI. From here you can extend the model, add authentication, or switch the storage layer to PostgreSQL for a more robust clone.
# Create a new todo
curl -X POST http://127.0.0.1:8000/todos \
-H 'Content-Type: application/json' \
-d '{"title":"Buy milk","description":"2% whole milk","completed":false}'
# List all todos
curl http://127.0.0.1:8000/todos
# Get the first todo (replace 1 with the actual id returned)
curl http://127.0.0.1:8000/todos/1| Check | Command / Why it matters |
|---|---|
| Virtual env activated and packages installed | pip list | grep -E 'fastapi|uvicorn' |
| Server starts without error | uvicorn main:app --reload (should show 'Uvicorn running on http://127.0.0.1:8000') |
| POST creates a todo and GET returns it | curl -X POST … && curl http://127.0.0.1:8000/todos (JSON array with your item) |
| Interactive docs load | Open http://127.0.0.1:8000/docs in a browser and see the Swagger UI |
Your minimal FastAPI Todo API is up and running. Feel free to add authentication, pagination, or switch to PostgreSQL for a more production‑grade clone.