FastAPI
FastAPI is a modern, high-performance web framework for building APIs with Python 3.8+ based on standard Python type hints. This guide covers setting up your environment, creating endpoints, and understanding how FastAPI documents your code automatically.
Introduction
π οΈ Setting Up Your Environment
Before writing code, ensure your Python environment is ready. It is best practice to use a virtual environment to manage dependencies.
- Create and activate a virtual environment:
- Install FastAPI and Uvicorn: Uvicorn is the ASGI server that will serve your FastAPI application.
π Creating Your First Endpoint
To define an API endpoint, you use a decorator (like @app.get()) to associate a URL path with a Python function.
Basic GET Endpoint
Here is how you define a GET endpoint that returns shipment data:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
async def get_items():
return [
{"id": 101, "item": "Keyboard", "status": "shipped"},
{"id": 102, "item": "Monitor", "status": "pending"}
]
Relative Paths
Notice you do not add the full URL (like http://localhost...) to the decorator. You provide the relative path (e.g., /items).
π§ͺ Testing Your API
Once your code is written in a file (e.g., main.py), you need to run the server to test it.
- Run the server:
--reload makes the server restart automatically when you change your code.
2. Test via Browser:
Open your browser and navigate to http://127.0.0.1:8000/items. You will see the JSON data returned.
3. Test via Terminal:
Automated Testing with TestClient
FastAPI provides a powerful TestClient (built on top of HTTPX) that allows you to write automated integration tests using Pytest. Instead of spinning up a live server, the TestClient invokes your FastAPI application directly in-memory, making tests extremely fast and reliable.
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_items():
response = client.get("/items")
assert response.status_code == 200
assert response.json() == [
{"id": 101, "item": "Keyboard", "status": "shipped"},
{"id": 102, "item": "Monitor", "status": "pending"}
]
Using TestClient ensures your routing logic, dependency injection, and endpoint functions are all tested together exactly as they would run in production.
π Understanding Automatic API Documentation
One of FastAPI's most powerful features is that it follows the OpenAPI standard to generate documentation automatically.
- Swagger UI: Accessible at
http://127.0.0.1:8000/docs. It provides an interactive interface to "Try it out" and call your endpoints directly from the browser. - ReDoc: Accessible at
http://127.0.0.1:8000/redoc. It provides a clean, organized alternative view of your API structure.
π Path Parameters
Path parameters are segments of the URL that act as variables. They are used to find specific information based on a location or ID.
Static Path
A static path is fixed and cannot be changed.
- Test:
curl -X GET "http://localhost:8000/items"
Dynamic Path
A dynamic path uses curly braces {} to capture values from the URL.
- Test:
curl -X GET "http://localhost:8000/items/computer" - Result: The API returns
{"item_id": "computer"}.
β οΈ The "Order Matters" Rule
FastAPI matches routes from top to bottom. If a dynamic path is defined before a static path that looks similar, the static path will be "shadowed" and never execute.
Always place static paths before dynamic paths:
# 1. Specific Static Path (Runs first)
@app.get("/items/favorite-item")
async def read_favorite_item():
return {"item": "This is the favorite!"}
# 2. Generic Dynamic Path (Runs as a fallback)
@app.get("/items/{item_id}")
async def read_item(item_id: str):
return {"item_id": item_id}
π Modular Routing & Architectural Scaling
As a backend application grows, placing all endpoints within a single main.py file creates a monolithic bottleneck that degrades code readability and developer experience. FastAPI resolves this via APIRouter modules.
APIRouter Architecture
Each subdirectory or resource area (e.g., website views, API endpoints) gets its own isolated router file.
# fastapi-app/routes/website/pages_route.py
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from config.templates import templates
router = APIRouter() # Decoupled router instance
@router.get("/", response_class=HTMLResponse)
async def root(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "website/homepage.html")
In the main entrypoint (main.py), you simply import and mount the routers:
# fastapi-app/main.py
from fastapi import FastAPI
from routes.website import pages_route
app = FastAPI()
app.include_router(pages_route.router)
Decoupling View Templates (Jinja2Templates)
When rendering web pages, views depend on a Jinja2Templates object. In smaller setups, this object is instantiated in main.py. However, importing it from main.py into router submodules causes circular imports:
main.pymust import the routers to register them (app.include_router(...)).- The routers must import
templatesfrommain.pyto call.TemplateResponse(...).
Circular Dependency Mitigation Pattern
To break this dependency loop, instantiate Jinja2Templates inside a third, independent configuration file (e.g. config/templates.py). Both main.py and your routers can safely import the shared template instance without spawning import loops.
Build-Time Serialization vs. Dynamic Scanning
When displaying dynamic graphs (like Zettelkasten networks), scanning the filesystem at runtime introduces severe performance costs and forces production containers to host raw Markdown notes.
A production-grade pattern compiles the Markdown folder structure locally via a build script (e.g. zettelkasten_graph.py) to a static JSON file (graph-data.json). The web router then serves this pre-serialized static file directly, ensuring zero filesystem latency in production.
πΎ Database Portability & Dual-Engine Fallback
In a professional development lifecycle, requiring a full external database server (like PostgreSQL) for basic local tests or quick scripting degrades the developer experience. A more robust architecture implements a portable dual-engine database setup.
This design dynamically selects the database driver depending on the running environment:
1. Docker Environment (Dev/Prod): Injected environment variables like POSTGRES_HOST or DATABASE_URL trigger the application to establish a connection to a PostgreSQL database container via psycopg.
2. Local Environment (CLI): In the absence of PostgreSQL environment variables, the application transparently falls back to a zero-dependency SQLite database file (maavita.db).
Portability Adapter Implementation
The connection adapter lazily imports PostgreSQL dependencies only when needed, allowing SQLite environments to run without installing additional native binary database libraries:
import os
from typing import Any
conn: Any = None
curs: Any = None
def get_db(name: str | None = None, reset: bool = False) -> None:
global conn, curs
...
database_url = os.getenv("DATABASE_URL")
postgres_host = os.getenv("POSTGRES_HOST")
if database_url or postgres_host:
# Lazy load psycopg for PostgreSQL environments
import psycopg
if database_url:
conn = psycopg.connect(database_url)
else:
conn = psycopg.connect(host=postgres_host, ...)
else:
# Fall back to zero-dependency SQLite
from sqlite3 import connect as sqlite_connect
conn = sqlite_connect(name or "maavita.db")
This ensures maximum environment portability: local CLI actions (like testing or manual migrations) stay lightweight, while production deployments enjoy the full performance and scalability of PostgreSQL.
π References
- Courses
-
Documentation
- Official FastAPI Documentation
- FastAPI Tutorial - User Guide
βοΈ Parameters
FastAPI allows various ways to send data to your endpoints, primarily through Path, Query, Request Body, and Headers.
URL / Path Parameters
These are variables that are part of the URL path, often used to identify a specific resource (like a user ID).
In the URL: http://127.0.0.1:8000/hello/Jefferson
Query Parameters
Query parameters are attached after a ? in a URL and separated by & (e.g., /search?query=example&limit=10). They are typically used to filter data based on the URL provided.
In the URL: http://127.0.0.1:8000/hello/?name=Jefferson
Note: GET requests are meant to be idempotent and cacheable. They donβt usually have a body because caches use the URL (including query params) to store responses.
Request Body
For requests that modify data (like POST, PUT, or PATCH), the body can carry complex data in JSON or other formats.
from fastapi import Body
@app.post("/hello/")
def hello(name: str = Body(embed=True)):
return {"message": f"Hello {name}"}
The embed=True parameter tells FastAPI to expect JSON formatted like {"name": "Jefferson"} rather than a raw string.
HTTP Header
You can also extract values directly from HTTP headers.