FastAPI Guide: Build a Production API Step by Step
A step-by-step FastAPI build: request validation with Pydantic models, response models that control what leaves the API, dependency injection, proper status codes and errors, tests with TestClient and dependency overrides, then the settings, workers and health checks a production deployment needs.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
FastAPI turns ordinary type-hinted Python functions into an HTTP API with request validation, JSON serialisation and interactive documentation. That makes the demo easy. This tutorial goes further, to the parts that decide whether the API survives real clients: strict input models, response models, dependencies you can replace in tests, proper errors, and the deployment settings that matter.
It builds a small products API. Install FastAPI with its standard extras, which include the server and the fastapi command: python -m pip install "fastapi[standard]".
Step 1: models first
Define what a client may send and what the API returns as Pydantic models. Constraints live in the model, not scattered through the endpoint code (FastAPI: request body):
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Query, status
from pydantic import BaseModel, Field
class ProductIn(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0)
stock: int = Field(default=0, ge=0)
class Product(ProductIn):
id: intSeparate input and output models matter. ProductIn has no id, so a client cannot choose its own ID; Product is what the API promises to return.
Step 2: a dependency for storage
Endpoints should not reach for a global database connection. They declare what they need with Depends, and FastAPI provides it for each request (FastAPI: dependencies). Here the "database" is an in-memory store, which a real application would replace with a database session:
class Store:
def __init__(self):
self.items: dict[int, Product] = {}
self.next_id = 1
def add(self, data: ProductIn) -> Product:
product = Product(id=self.next_id, **data.model_dump())
self.items[product.id] = product
self.next_id += 1
return product
_store = Store()
def get_store() -> Store:
return _store
StoreDep = Annotated[Store, Depends(get_store)]Step 3: endpoints with explicit status codes and errors
app = FastAPI(title="Products API", version="1.0.0")
@app.post("/products", status_code=status.HTTP_201_CREATED)
def create_product(data: ProductIn, db: StoreDep) -> Product:
return db.add(data)
@app.get("/products/{product_id}")
def read_product(product_id: int, db: StoreDep) -> Product:
if product_id not in db.items:
raise HTTPException(status_code=404, detail="Product not found")
return db.items[product_id]
@app.get("/products")
def list_products(
db: StoreDep,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
in_stock: bool = False,
) -> list[Product]:
items = [p for p in db.items.values() if p.stock > 0 or not in_stock]
return items[:limit]
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}Every type hint is doing work. product_id: int rejects /products/abc. The Query(ge=1, le=100) caps page size, so no client can ask for a million rows. The return annotation (-> Product) is the response model: FastAPI validates the output against it and drops any field not in the model (FastAPI: response model), which is how internal fields such as a password hash stay out of responses.
Step 4: exercise it with TestClient
TestClient sends requests straight to the application, with no server running (FastAPI: testing):
from fastapi.testclient import TestClient
client = TestClient(app)
r = client.post("/products", json={"name": "Keyboard", "price": 1499, "stock": 3})
print(r.status_code, r.json())
r = client.post("/products", json={"name": "", "price": -5})
print(r.status_code, [(e["loc"][-1], e["msg"]) for e in r.json()["detail"]])
print(client.get("/products/99").status_code, client.get("/products/99").json())
print(client.get("/products?limit=500").status_code)201 {'name': 'Keyboard', 'price': 1499.0, 'stock': 3, 'id': 1}
422 [('name', 'String should have at least 1 character'), ('price', 'Input should be greater than 0')]
404 {'detail': 'Product not found'}
422The invalid product returns both problems at once, with the field each belongs to, and the handler never ran. That behaviour came from the model definitions alone.
Tests must not share state with each other or with production. dependency_overrides swaps a dependency for the duration of a test, which is why storage was a dependency in the first place:
app.dependency_overrides[get_store] = lambda: Store() # a fresh, empty store
print(client.get("/products").json())
app.dependency_overrides.clear()
print(len(client.get("/products").json()))[]
1In a pytest suite, put the override in a fixture so every test starts clean; unit testing with pytest covers fixtures.
Step 5: what production adds
The code above is correct; production is about how it runs. FastAPI's deployment guide frames this as a set of concerns: HTTPS, starting on boot, restarting after crashes, running several processes, and memory (FastAPI: deployment concepts).
fastapi dev main.py # development: auto-reload
fastapi run main.py --workers 4 # production: several worker processes
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 # the same, with Uvicorn directly- Settings from the environment. Database URLs and keys come from environment variables, loaded once at startup; see environment variables and secrets in Python apps.
- def or async def. FastAPI runs plain
defendpoints in a threadpool, so blocking database drivers are fine there.async defendpoints run on the event loop and must never call blocking code (FastAPI: concurrency); the asyncio guide explains why. - Workers. One process uses one CPU core for Python code. Run several workers, or several containers behind a load balancer.
- A health endpoint like
/healthabove, for the load balancer or orchestrator to poll. - HTTPS at the edge, usually terminated by a reverse proxy or the platform, and CORS configured only for the front-end origins that need it.
- A container image. Package the app with pinned dependencies; the FastAPI and Docker guide for ML models shows the pattern with a model behind the endpoint.
Growing beyond one file
A real service outgrows a single module quickly. FastAPI's APIRouter groups related endpoints, so each resource gets its own module, for example routers/products.py and routers/orders.py, each creating a router with a shared prefix and tags. main.py then only creates the app and includes the routers with app.include_router(...). Models, the database session dependency and settings live in their own modules, imported where needed. The layout conventions in how to structure a Python project apply unchanged.
Authentication
FastAPI provides building blocks rather than a finished user system. Security dependencies such as OAuth2PasswordBearer or HTTPBearer extract a token from the request, and your own dependency verifies it and returns the current user. Endpoints that need a signed-in user simply declare that dependency, and the generated documentation shows which endpoints are protected. Keep password hashing, token signing keys and expiry settings out of the code, and prefer an established identity provider over hand-written login flows when the project allows it.
Errors clients can handle
HTTPException covers most cases. For a consistent error body across the whole API, register exception handlers with @app.exception_handler(...) that convert your own exception classes into responses. A domain error such as "out of stock" then becomes a 409 with the same JSON shape everywhere, instead of each endpoint formatting its own.
Common mistakes
- One model for input and output, which lets clients set fields like
idoris_admin. - Calling
requestsor a synchronous database driver insideasync defendpoints. - Tests that hit the real database because storage was imported, not injected.
- Returning 200 with
{"error": ...}instead of a real 4xx status. - Doing slow work, such as sending email or building a report, inside the request.
BackgroundTasksruns small jobs after the response is sent; heavy or must-not-lose work belongs in a task queue. - Leaving the interactive
/docspage open on a public production API that should not advertise its endpoints. Passdocs_url=Noneandredoc_url=NonetoFastAPI(...), or put the docs behind authentication.
For how FastAPI compares with Flask and Django for different projects, see Flask vs FastAPI vs Django; for the design rules behind these endpoints, see building REST APIs with Python. API development with Flask or FastAPI appears in the model deployment modules of the Data Science and MLOps courses.
Sources
- Request Body β FastAPI documentation
- Response Model - Return Type β FastAPI documentation
- Dependencies β FastAPI documentation
- Testing β FastAPI documentation
- Concurrency and async / await β FastAPI documentation
- Deployments Concepts β FastAPI documentation
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecksβ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Related articles
Python Mini Projects to Build in a Weekend
Weekend projects fail when they are too big to finish by Sunday night. The fix is scoping: a core you can build on Saturday and improvements for Sunday. Three mini projects are scoped that way here, a weather and bike-rental explorer, a safe file organiser and a habit tracker, with each Saturday core built and run.
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.