Skip to content

PYTHON FASTAPI · LESSON 8 OF 8

Run and deploy the finished API

Package the application and understand the operational requirements of a public API.

  • Uvicorn
  • HTTPS
  • persistence
  • deployment

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, requirements.txt and README.md. Stop any earlier server before running this example.

Create and activate a virtual environment as shown in lesson 1. Install with python -m pip install -r requirements.txt, then run fastapi dev main.py. Lessons 6–8 require TASK_API_KEY as explained in lesson 6.

1. Understand the concepts

First run python -m pytest -q. For a production-style local run use python -m uvicorn main:app --host 0.0.0.0 --port 8000. main is the module name and app is the FastAPI object. Do not use --reload in production.

Choose a Python web-service host with persistent disk support for this SQLite demo. Upload main.py and requirements.txt through your repository, select Python 3.12 or 3.13, and set the build command to python -m pip install -r requirements.txt. A typical start command is python -m uvicorn main:app --host 0.0.0.0 --port "$PORT"; use the host-provided port setting.

Configure TASK_API_KEY in the hosting secret settings. Mount persistent storage and set TASK_DB to an absolute file path inside that mount, such as /data/tasks.db. Keep one service instance for this demo. If your host has an ephemeral filesystem or needs multiple replicas, migrate to a managed database such as PostgreSQL before publishing.

Use the host HTTPS URL and set /health as the health-check path. Test health without a key, task access without a key (401), and CRUD with the key. Restart the service and confirm the task survives. Persistence must be tested, not assumed.

Back up the database using SQLite backup tooling, test restoration and monitor error logs. Avoid logging credentials. Pin the exact dependency versions you validated with python -m pip freeze > requirements.lock.txt and use that file for repeatable builds. Schema migrations, user accounts and rate limiting are next steps before offering this as a multi-user product.

2. Build and run

These are complete files, not fragments. Save them beside each other in the extracted project folder. Open http://127.0.0.1:8000/docs to send requests.

main.py

main.py
import os
import sqlite3
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query, Response
from fastapi.security import APIKeyHeader
import secrets
from pydantic import BaseModel, ConfigDict, Field

DB_PATH = os.getenv("TASK_DB", "tasks.db")

def get_db():
    connection = sqlite3.connect(DB_PATH, check_same_thread=False)
    connection.row_factory = sqlite3.Row
    try:
        yield connection
    finally:
        connection.close()

@asynccontextmanager
async def lifespan(app: FastAPI):
    connection = sqlite3.connect(DB_PATH)
    try:
        connection.execute("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)")
        connection.commit()
    finally:
        connection.close()
    yield

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

def require_api_key(key: Annotated[str | None, Depends(api_key_header)]):
    expected = os.getenv("TASK_API_KEY")
    if not expected:
        raise HTTPException(status_code=503, detail="API key is not configured")
    if not key or not secrets.compare_digest(key, expected):
        raise HTTPException(status_code=401, detail="Invalid API key")

app = FastAPI(title="EduPoly Task API", lifespan=lifespan)
Database = Annotated[sqlite3.Connection, Depends(get_db)]

class TaskCreate(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    title: str = Field(min_length=1, max_length=120)
    done: bool = False

class TaskRead(TaskCreate):
    id: int

def find_task(db, task_id):
    row = db.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
    if row is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return dict(row)

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/tasks", response_model=TaskRead, status_code=201, dependencies=[Depends(require_api_key)])
def create_task(task: TaskCreate, db: Database):
    cursor = db.execute("INSERT INTO tasks (title, done) VALUES (?, ?)", (task.title, task.done))
    db.commit()
    return find_task(db, cursor.lastrowid)

@app.get("/tasks", response_model=list[TaskRead], dependencies=[Depends(require_api_key)])
def list_tasks(db: Database, limit: Annotated[int, Query(ge=1, le=100)] = 20):
    return [dict(row) for row in db.execute("SELECT * FROM tasks ORDER BY id LIMIT ?", (limit,))]

@app.get("/tasks/{task_id}", response_model=TaskRead, dependencies=[Depends(require_api_key)])
def read_task(task_id: int, db: Database):
    return find_task(db, task_id)

@app.put("/tasks/{task_id}", response_model=TaskRead, dependencies=[Depends(require_api_key)])
def replace_task(task_id: int, task: TaskCreate, db: Database):
    find_task(db, task_id)
    db.execute("UPDATE tasks SET title = ?, done = ? WHERE id = ?", (task.title, task.done, task_id))
    db.commit()
    return find_task(db, task_id)

@app.delete("/tasks/{task_id}", status_code=204, dependencies=[Depends(require_api_key)])
def delete_task(task_id: int, db: Database):
    find_task(db, task_id)
    db.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
    db.commit()
    return Response(status_code=204)

requirements.txt

requirements.txt
fastapi[standard]>=0.115,<1
pydantic>=2,<3
httpx>=0.27,<1
pytest>=8,<10

test_main.py

test_main.py
import pytest
from fastapi.testclient import TestClient
import main

@pytest.fixture
def client(tmp_path, monkeypatch):
    monkeypatch.setattr(main, "DB_PATH", str(tmp_path / "test.db"))
    monkeypatch.setenv("TASK_API_KEY", "test-only-key")
    with TestClient(main.app) as session:
        session.headers["X-API-Key"] = "test-only-key"
        yield session

def test_task_lifecycle(client):
    created = client.post("/tasks", json={"title": "Learn FastAPI"})
    assert created.status_code == 201
    task_id = created.json()["id"]
    assert created.json()["done"] is False
    assert client.get(f"/tasks/{task_id}").status_code == 200
    updated = client.put(f"/tasks/{task_id}", json={"title": "Finished", "done": True})
    assert updated.json()["done"] is True
    assert len(client.get("/tasks").json()) == 1
    deleted = client.delete(f"/tasks/{task_id}")
    assert deleted.status_code == 204
    assert deleted.content == b""
    assert client.get(f"/tasks/{task_id}").status_code == 404

def test_validation(client):
    assert client.post("/tasks", json={"title": "  "}).status_code == 422
    assert client.post("/tasks", json={"title": "Valid", "typo": 1}).status_code == 422
    assert client.get("/tasks?limit=101").status_code == 422

def test_security(client):
    client.headers.pop("X-API-Key")
    assert client.get("/tasks").status_code == 401
    assert client.get("/health").status_code == 200
    client.headers["X-API-Key"] = "wrong"
    assert client.post("/tasks", json={"title": "Blocked"}).status_code == 401

def test_missing_configuration(client, monkeypatch):
    monkeypatch.delenv("TASK_API_KEY")
    assert client.get("/tasks").status_code == 503

3. Check the result

  • Four tests pass before release.
  • The HTTPS health URL returns {"status":"ok"}.
  • Requests without a key are rejected and a created task survives a service restart.

4. Practice independently

Describe what would happen if TASK_DB points to temporary storage.

Show the explanation

Tasks could disappear on redeploy or restart when the host replaces the filesystem. Attach persistent storage or use an external database.

When something goes wrong

For import errors, confirm the virtual environment is active and install requirements.txt using that environment’s Python. For 422, read the response detail and compare your input to the model. For 404, check the method, path and resource id. For 401 in later lessons, use Authorize in /docs with the same key configured on the server.

Read the related official FastAPI documentation ↗