When it comes to building high-performance REST APIs in Python, FastAPI has quickly become the framework of choice for modern backend developers. It is blazing fast (on par with Node.js and Go), has first-class support for async programming, and automatically generates interactive API documentation — all out of the box.
In this tutorial, we will build a fully functional REST API from scratch using FastAPI. We will cover project setup, Pydantic data validation, async route handlers, JWT-based authentication, and how to run and test the server using the built-in Swagger UI.
Step 1: Project Setup
Start by creating a new directory and setting up a Python virtual environment. This isolates your project's dependencies from the rest of your system.
mkdir fastapi-project
cd fastapi-project
python -m venv .venv
# Activate the environment
# On macOS / Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate
Now install FastAPI along with uvicorn, which is the ASGI server that will run our
application:
pip install fastapi "uvicorn[standard]" python-jose[cryptography] passlib[bcrypt]
Create the main application file:
# main.py
from fastapi import FastAPI
app = FastAPI(
title="My REST API",
description="A production-ready REST API built with FastAPI.",
version="1.0.0",
)
You can verify everything is working by running uvicorn main:app --reload and visiting
http://127.0.0.1:8000 in your browser.
Step 2: Pydantic Models & Validation
FastAPI uses Pydantic for data validation and serialization. You define schemas as Python classes, and FastAPI enforces them automatically on every request and response. This eliminates a huge class of bugs and means you never have to write manual validation logic.
Create a schemas.py file:
# schemas.py
from pydantic import BaseModel, Field
from typing import Optional
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="The name of the item")
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0, description="Price must be greater than zero")
in_stock: bool = True
class ItemResponse(ItemCreate):
id: int
class Config:
from_attributes = True
The Field function lets you add constraints like min_length, gt
(greater than), and human-readable descriptions that will appear in your auto-generated API docs. If any
incoming request violates these rules, FastAPI automatically returns a clear
422 Unprocessable Entity error.
Step 3: Routes & CRUD Operations
Now let's define our API routes. We'll use an in-memory dictionary to simulate a database so we can focus on the FastAPI concepts without database boilerplate.
# main.py (updated)
from fastapi import FastAPI, HTTPException
from schemas import ItemCreate, ItemResponse
from typing import list
app = FastAPI(title="Items API", version="1.0.0")
# Simulated in-memory database
db: dict[int, ItemResponse] = {}
_next_id = 1
@app.get("/items", response_model=list[ItemResponse], tags=["Items"])
async def list_items():
"""Return all items from the database."""
return list(db.values())
@app.post("/items", response_model=ItemResponse, status_code=201, tags=["Items"])
async def create_item(payload: ItemCreate):
"""Create a new item."""
global _next_id
item = ItemResponse(id=_next_id, **payload.model_dump())
db[_next_id] = item
_next_id += 1
return item
@app.get("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
async def get_item(item_id: int):
"""Retrieve a single item by its ID."""
if item_id not in db:
raise HTTPException(status_code=404, detail="Item not found")
return db[item_id]
@app.put("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
async def update_item(item_id: int, payload: ItemCreate):
"""Update an existing item."""
if item_id not in db:
raise HTTPException(status_code=404, detail="Item not found")
updated = ItemResponse(id=item_id, **payload.model_dump())
db[item_id] = updated
return updated
@app.delete("/items/{item_id}", status_code=204, tags=["Items"])
async def delete_item(item_id: int):
"""Delete an item by its ID."""
if item_id not in db:
raise HTTPException(status_code=404, detail="Item not found")
del db[item_id]
Notice the async def keyword — this is one of FastAPI's superpowers. By writing asynchronous
route handlers, your API can handle thousands of concurrent connections without blocking while waiting for
I/O operations like database queries.
Step 4: JWT Authentication
Most real-world APIs need to protect their endpoints. We'll implement a standard JSON Web Token
(JWT) authentication flow. When a user logs in with their credentials, the server returns a
signed JWT. The client then sends this token in the Authorization header on subsequent
requests.
# auth.py
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
SECRET_KEY = "your-very-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
# Fake user database
FAKE_USERS_DB = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("secret"),
}
}
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
return username
Now add the login endpoint to main.py and protect your routes using the Depends
mechanism:
# In main.py
from fastapi.security import OAuth2PasswordRequestForm
from auth import create_access_token, get_current_user, FAKE_USERS_DB, pwd_context
@app.post("/auth/token", tags=["Auth"])
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = FAKE_USERS_DB.get(form_data.username)
if not user or not pwd_context.verify(form_data.password, user["hashed_password"]):
raise HTTPException(status_code=400, detail="Incorrect username or password")
token = create_access_token(data={"sub": form_data.username})
return {"access_token": token, "token_type": "bearer"}
# Protect an endpoint
@app.get("/me", tags=["Auth"])
async def read_current_user(current_user: str = Depends(get_current_user)):
return {"username": current_user}
Step 5: Testing & Auto-Generated Docs
One of FastAPI's most compelling features is its automatic, interactive documentation. Start your server and visit these URLs:
uvicorn main:app --reload
- Swagger UI:
http://127.0.0.1:8000/docs— Try out every endpoint directly in your browser. - ReDoc:
http://127.0.0.1:8000/redoc— A cleaner, read-only documentation view.
For automated tests, use pytest along with FastAPI's built-in TestClient:
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_item():
response = client.post("/items", json={"name": "Widget", "price": 9.99})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
assert data["id"] == 1
def test_get_item_not_found():
response = client.get("/items/999")
assert response.status_code == 404
Run all tests with:
pytest -v
Conclusion
FastAPI is a remarkably well-designed framework that makes building robust, high-performance REST APIs in Python a genuine pleasure. The combination of automatic data validation via Pydantic, native async support, and zero-effort interactive documentation means you spend less time on boilerplate and more time building actual features.
From here, you can extend this API by connecting a real database using SQLAlchemy or
Tortoise ORM, adding background tasks with BackgroundTasks, or deploying to
production with Docker and a reverse proxy like Nginx.