FastAPI is the modern way to build an API in Python: you declare what your data looks like, and it handles validation, JSON, and — the party trick — an interactive documentation page generated from your code. This build is a complete CRUD API for "notes", backed by Python's built-in sqlite3, so there's no separate database to run. By the end you have five working endpoints and a live /docs page you can click through.
| At a glance | |
|---|---|
| You need | A server running the Python application |
| Plan | Any plan — free runs while your session timer has time left, premium runs 24/7 |
| Time | About twenty minutes |
| No server yet? | Create your first app server |
For the Python basics — the app.py entry file, requirements.txt auto-install, and the SERVER_PORT rule — see Python on Falix. For a gentler FastAPI intro, FastAPI on Falix.
Step 1 — The files
requirements.txt:
fastapi
uvicorn
FastAPI is the framework; uvicorn is the server that runs it. sqlite3 is part of Python's standard library, so it needs no line here.
app.py:
import os
import sqlite3
from contextlib import closing
import uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
DB = "notes.db"
def init_db():
with closing(sqlite3.connect(DB)) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT ''
)
"""
)
conn.commit()
def connect():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
class NoteIn(BaseModel):
title: str
body: str = ""
app = FastAPI(title="Notes API")
init_db()
@app.get("/notes")
def list_notes():
with closing(connect()) as conn:
rows = conn.execute("SELECT * FROM notes ORDER BY id").fetchall()
return [dict(r) for r in rows]
@app.post("/notes", status_code=201)
def create_note(note: NoteIn):
with closing(connect()) as conn:
cur = conn.execute(
"INSERT INTO notes (title, body) VALUES (?, ?)",
(note.title, note.body),
)
conn.commit()
return {"id": cur.lastrowid, **note.model_dump()}
@app.get("/notes/{note_id}")
def get_note(note_id: int):
with closing(connect()) as conn:
row = conn.execute("SELECT * FROM notes WHERE id = ?", (note_id,)).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Note not found")
return dict(row)
@app.put("/notes/{note_id}")
def update_note(note_id: int, note: NoteIn):
with closing(connect()) as conn:
cur = conn.execute(
"UPDATE notes SET title = ?, body = ? WHERE id = ?",
(note.title, note.body, note_id),
)
conn.commit()
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="Note not found")
return {"id": note_id, **note.model_dump()}
@app.delete("/notes/{note_id}")
def delete_note(note_id: int):
with closing(connect()) as conn:
cur = conn.execute("DELETE FROM notes WHERE id = ?", (note_id,))
conn.commit()
if cur.rowcount == 0:
raise HTTPException(status_code=404, detail="Note not found")
return {"deleted": note_id}
if __name__ == "__main__":
port = int(os.environ.get("SERVER_PORT", 8080))
print(f"Listening on port {port}", flush=True)
uvicorn.run(app, host="0.0.0.0", port=port)
Step 2 — How it works
Five endpoints cover the full lifecycle of a resource:
| Endpoint | Method | Does |
|---|---|---|
/notes |
GET | List all notes |
/notes |
POST | Create a note (returns 201) |
/notes/{id} |
GET | Fetch one, or 404 |
/notes/{id} |
PUT | Replace one, or 404 |
/notes/{id} |
DELETE | Remove one, or 404 |
The FastAPI-specific parts:
NoteIn(BaseModel)declares the shape of an incoming note. FastAPI validates every request body against it automatically — send a note without atitleand you get a 422 with a precise error, before your code runs. You never write validation by hand.- Type hints do double duty.
note_id: intin the path means FastAPI parses and validates the id for you; a non-numeric id is rejected automatically. raise HTTPException(status_code=404, ...)is how you return a proper error response — it becomes clean JSON.- Parameterized SQL (
?placeholders) throughout keeps user input as data, safe from injection. uvicorn.run(app, host="0.0.0.0", port=port)binds the public port — the one Falix rule every web app follows.
🎯 The free part: Because your routes and models are typed, FastAPI generates interactive API docs at
/docsand a machine-readable schema at/openapi.json— no extra code./docslets you try every endpoint from the browser.
Step 3 — Start it and exercise the API
Press Start. The console installs FastAPI and uvicorn, then prints Listening on port …. Open your server's address (from the Network page):
- Visit
/docs— the interactive documentation. ExpandPOST /notes, click Try it out, send a note, and watch it come back with anid. - The endpoints work from any client too:
# Create
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Buy milk","body":"2%"}' http://YOUR_ADDRESS:PORT/notes
# List
curl http://YOUR_ADDRESS:PORT/notes
# Update, then delete
curl -X PUT -H "Content-Type: application/json" \
-d '{"title":"Buy oat milk","body":"barista"}' http://YOUR_ADDRESS:PORT/notes/1
curl -X DELETE http://YOUR_ADDRESS:PORT/notes/1
A GET /notes/999 on a note that doesn't exist returns 404 — the error path working as designed.
Where the data lives
Notes are stored in notes.db in the server's files.
⚠️ Heads up: A reinstall or an application switch wipes the server's files,
notes.dbincluded, and it isn't rebuilt for you. Back it up before risky changes, or move storage to a managed database for durability — swap thesqlite3calls for a driver as in Connect your app to a database; the routes don't change.
Troubleshooting
ModuleNotFoundError: No module named 'fastapi'(oruvicorn) — the install didn't finish. Confirm both are inrequirements.txt, or add them from the Packages page, then restart.422 Unprocessable Entity— that's validation working: the request body didn't matchNoteIn. The response says which field is wrong. Send valid JSON with atitle./docsloads but looks unstyled — it pulls its interface assets from a CDN, so a strict network could affect the styling; the API itself (/notes,/openapi.json) is unaffected and fully functional.- Page won't load / connection refused — port or bind issue: confirm
uvicorn.run(..., host="0.0.0.0", port=port)withportfromSERVER_PORT; see I can't reach my app.
Response models, dependencies, auth, and async routes are all standard FastAPI — the official docs at fastapi.tiangolo.com go deep.