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
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
fastapi[standard]>=0.115,<1
pydantic>=2,<3
httpx>=0.27,<1
pytest>=8,<10
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.
