Build a FastAPI CRUD API

A complete REST API with FastAPI and Python's built-in sqlite3 — create, read, update, delete, with request validation and an interactive /docs page you get for free. Verified end to end with curl.

Skip the setup This guide has a one-click starter template that installs everything below onto your server.

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 a title and 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: int in 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 /docs and a machine-readable schema at /openapi.json — no extra code. /docs lets 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. Expand POST /notes, click Try it out, send a note, and watch it come back with an id.
  • 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.db included, and it isn't rebuilt for you. Back it up before risky changes, or move storage to a managed database for durability — swap the sqlite3 calls for a driver as in Connect your app to a database; the routes don't change.

Troubleshooting

  • ModuleNotFoundError: No module named 'fastapi' (or uvicorn) — the install didn't finish. Confirm both are in requirements.txt, or add them from the Packages page, then restart.
  • 422 Unprocessable Entity — that's validation working: the request body didn't match NoteIn. The response says which field is wrong. Send valid JSON with a title.
  • /docs loads 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) with port from SERVER_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.


Next steps

Was this guide helpful?