Stage 7 · Master
HTTP APIs & REST
Building APIs with FastAPI
Routes, path/query params, Pydantic models, validation, and OpenAPI docs.
Why FastAPI
FastAPI is the standard for building REST APIs in Python. It is fast (built on Starlette and uvicorn), has automatic OpenAPI docs, and uses Pydantic for request/response validation. For DevOps tooling, it is the obvious choice.
FastAPI uses Python type hints to validate requests, generate documentation, and serialize responses. If the type system describes it, FastAPI handles it automatically.
Hello World
from fastapi import FastAPI
app = FastAPI(title="DevOps Toolkit API", version="1.0.0")
@app.get("/health")
def health_check():
return {"status": "healthy"}
@app.get("/")
def root():
return {"message": "DevOps Toolkit API"}
# Run with: uvicorn main:app --reload
# The --reload flag watches for file changes during developmentuvicorn is the ASGI server that runs FastAPI. --reload watches for file changes. For production, use uvicorn main:app --workers 4.
Path and Query Parameters
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
# Path parameter — extracted from URL
@app.get("/servers/{server_name}")
def get_server(server_name: str):
return {"name": server_name, "status": "healthy"}
# Query parameters — ?key=value in URL
@app.get("/servers")
def list_servers(
env: str = Query(default="production", description="Filter by environment"),
status: Optional[str] = Query(default=None, description="Filter by status"),
limit: int = Query(default=100, ge=1, le=1000),
):
return {"env": env, "status": status, "limit": limit}
# Multiple path parameters
@app.get("/servers/{namespace}/{pod_name}")
def get_pod(namespace: str, pod_name: str):
return {"namespace": namespace, "pod": pod_name}FastAPI extracts path parameters from the URL and query parameters from the query string. Type hints define the expected types. Invalid types return 422 errors automatically.
Request Body with Pydantic
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class DeployRequest(BaseModel):
service: str = Field(description="Service name to deploy")
image: str = Field(description="Docker image to deploy")
replicas: int = Field(default=1, ge=1, le=100, description="Number of replicas")
env: str = Field(default="staging", description="Target environment")
class DeployResponse(BaseModel):
deployment_id: str
status: str
message: str
@app.post("/deploy", response_model=DeployResponse)
def deploy(req: DeployRequest):
# req is a validated DeployRequest object
return DeployResponse(
deployment_id="deploy-123",
status="started",
message=f"Deploying {req.image} to {req.env}",
)The request body must be JSON. FastAPI validates it against the Pydantic model and returns 422 with detailed error messages if validation fails.
Response Models
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class ServerInternal(BaseModel):
name: str
ip: str
status: str
cpu_percent: float
password_hash: str # Internal field — never expose this
class ServerPublic(BaseModel):
name: str
status: str
cpu_percent: float
@app.get("/servers/{name}", response_model=ServerPublic)
def get_server(name: str) -> ServerInternal:
# Return internal model, FastAPI strips extra fields
return ServerInternal(
name=name, ip="10.0.1.1", status="healthy",
cpu_percent=45.0, password_hash="...",
)response_model controls what fields are returned. Internal fields like password_hash are automatically stripped. This prevents accidental data leakage.
Error Handling
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
class ServiceNotFoundError(Exception):
def __init__(self, service: str):
self.service = service
@app.exception_handler(ServiceNotFoundError)
def handle_not_found(req, exc):
return JSONResponse(
status_code=404,
content={"error": f"Service '{exc.service}' not found"},
)
@app.get("/services/{name}")
def get_service(name: str):
if name not in ["web-01", "api-01"]:
raise ServiceNotFoundError(name)
return {"name": name, "status": "healthy"}
# Use HTTPException for simple cases
@app.get("/admin/config")
def get_admin_config(authorization: str = None):
if not authorization:
raise HTTPException(status_code=401, detail="Missing authorization")
return {"key": "value"}HTTPException is for simple error responses. For domain-specific errors, create custom exception classes and register handlers. This keeps error logic centralized.
OpenAPI Documentation
FastAPI generates OpenAPI docs automatically from your type hints and Pydantic models. Access them at /docs (Swagger UI) and /redoc (ReDoc).
from fastapi import FastAPI
app = FastAPI(
title="DevOps Toolkit API",
description="Internal API for managing services and deployments",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
# Tags for organizing endpoints
@app.get("/health", tags=["monitoring"])
def health():
return {"status": "ok"}
@app.post("/deploy", tags=["deployment"])
def deploy():
return {"status": "started"}
# Disable docs in production
# app = FastAPI(docs_url=None, redoc_url=None)The /docs endpoint gives your team an interactive API explorer. In production, disable docs with docs_url=None to avoid exposing internal APIs.
uvicorn --reload is great for development. For production, use gunicorn with uvicorn workers: gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.