
If you’re looking to build a fast, modern REST API with minimal boilerplate and excellent performance, FastAPI is the framework to learn. It has rapidly become the go-to choice for Python developers building APIs—and for good reason.
In this guide, you’ll learn how to build a production-ready REST API in Python using FastAPI. We’ll cover everything from basic setup through authentication, database integration, error handling, and deployment considerations.
Why Use FastAPI for REST APIs
FastAPI is a high-performance web framework for building APIs with Python 3.7+. Built on top of Starlette for the web parts and Pydantic for data validation, it offers a compelling combination of features.
Automatic Documentation
FastAPI generates interactive API documentation automatically. Both Swagger UI and ReDoc are available out of the box, making it easy for frontend developers and API consumers to understand and test your endpoints without additional tooling.
Type Hints and Validation
Python type hints aren’t just for documentation—FastAPI uses them for automatic request validation, serialization, and IDE support. This reduces bugs and speeds up development significantly.
Async Support
FastAPI supports both synchronous and asynchronous request handlers. For I/O-bound operations like database queries or external API calls, async handlers can handle thousands of concurrent requests efficiently.
Performance
FastAPI is one of the fastest Python frameworks available, comparable to Node.js and Go in benchmarks. This performance comes from Starlette’s ASGI foundation and efficient request handling.
Setting Up Your FastAPI Project
Let’s start with a proper project structure that scales well as your API grows.
Create Project Structure
# Create project directory
mkdir fastapi-project
cd fastapi-project
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install fastapi uvicorn[standard] pydantic sqlalchemy python-dotenv
Organize your project with a scalable folder structure:
fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── item.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── item.py
│ ├── routers/
│ │ ├── __init__.py
│ │ └── items.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── item_service.py
│ └── database.py
├── tests/
│ └── test_items.py
├── requirements.txt
└── .env
Create the Main Application
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import items
from app.config import settings
app = FastAPI(
title="My REST API",
description="A production-ready REST API built with FastAPI",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# Configure CORS for frontend applications
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(items.router, prefix="/api/v1", tags=["items"])
@app.get("/")
async def root():
return {"message": "Welcome to the FastAPI REST API", "version": "1.0.0"}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
Configuration Management
Use environment variables for configuration to keep secrets out of your codebase:
# app/config.py
from pydantic_settings import BaseSettings
from typing import List
class Settings(BaseSettings):
app_name: str = "FastAPI REST API"
debug: bool = False
database_url: str = "sqlite:///./app.db"
secret_key: str = "change-this-in-production"
allowed_origins: List[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
settings = Settings()
Defining Data Models with Pydantic
Pydantic schemas define the shape of your request and response data. They provide automatic validation, serialization, and documentation.
# app/schemas/item.py
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime
class ItemBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="Item name")
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0, description="Price must be greater than 0")
is_available: bool = True
@validator('name')
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty or whitespace')
return v.strip()
class ItemCreate(ItemBase):
"""Schema for creating a new item"""
pass
class ItemUpdate(BaseModel):
"""Schema for updating an item - all fields optional"""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
price: Optional[float] = Field(None, gt=0)
is_available: Optional[bool] = None
class ItemResponse(ItemBase):
"""Schema for item responses - includes database fields"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True # Enables ORM mode for SQLAlchemy models
class ItemList(BaseModel):
"""Schema for paginated item list"""
items: list[ItemResponse]
total: int
page: int
per_page: int
Building CRUD Endpoints
Let’s create a complete CRUD router for items with proper error handling and pagination.
# app/routers/items.py
from fastapi import APIRouter, HTTPException, Depends, Query, status
from typing import List
from app.schemas.item import ItemCreate, ItemUpdate, ItemResponse, ItemList
from app.services.item_service import ItemService
router = APIRouter()
def get_item_service() -> ItemService:
return ItemService()
@router.get("/items", response_model=ItemList)
async def get_items(
page: int = Query(1, ge=1, description="Page number"),
per_page: int = Query(10, ge=1, le=100, description="Items per page"),
search: str = Query(None, description="Search in name and description"),
service: ItemService = Depends(get_item_service)
):
"""Get paginated list of items with optional search"""
items, total = await service.get_items(page, per_page, search)
return ItemList(
items=items,
total=total,
page=page,
per_page=per_page
)
@router.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(
item_id: int,
service: ItemService = Depends(get_item_service)
):
"""Get a single item by ID"""
item = await service.get_item(item_id)
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with id {item_id} not found"
)
return item
@router.post("/items", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(
item: ItemCreate,
service: ItemService = Depends(get_item_service)
):
"""Create a new item"""
return await service.create_item(item)
@router.put("/items/{item_id}", response_model=ItemResponse)
async def update_item(
item_id: int,
item: ItemUpdate,
service: ItemService = Depends(get_item_service)
):
"""Update an existing item"""
updated_item = await service.update_item(item_id, item)
if not updated_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with id {item_id} not found"
)
return updated_item
@router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(
item_id: int,
service: ItemService = Depends(get_item_service)
):
"""Delete an item"""
deleted = await service.delete_item(item_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with id {item_id} not found"
)
return None
Database Integration with SQLAlchemy
Most production APIs need database persistence. Here’s how to integrate SQLAlchemy with FastAPI.
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.config import settings
SQLALCHEMY_DATABASE_URL = settings.database_url
# For SQLite, add check_same_thread=False
if SQLALCHEMY_DATABASE_URL.startswith("sqlite"):
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# app/models/item.py
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime
from sqlalchemy.sql import func
from app.database import Base
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, index=True)
description = Column(String(500), nullable=True)
price = Column(Float, nullable=False)
is_available = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
Error Handling and Custom Exceptions
Proper error handling improves API usability and debugging. Create custom exception handlers for consistent error responses.
# app/exceptions.py
from fastapi import Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
class APIException(Exception):
def __init__(self, status_code: int, detail: str, error_code: str = None):
self.status_code = status_code
self.detail = detail
self.error_code = error_code or "UNKNOWN_ERROR"
class ItemNotFoundException(APIException):
def __init__(self, item_id: int):
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with id {item_id} not found",
error_code="ITEM_NOT_FOUND"
)
# Exception handlers to add to main.py
async def api_exception_handler(request: Request, exc: APIException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.error_code,
"message": exc.detail,
"path": str(request.url)
}
}
)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
errors.append({
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
"type": error["type"]
})
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": errors
}
}
)
Authentication with JWT
Most APIs require authentication. Here’s a basic JWT implementation:
# app/auth.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
from app.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
class TokenData(BaseModel):
user_id: Optional[int] = None
username: Optional[str] = None
def create_access_token(data: dict, expires_delta: timedelta = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.secret_key, algorithm="HS256")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=["HS256"])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
return TokenData(user_id=user_id)
except JWTError:
raise credentials_exception
Use the dependency in protected routes:
@router.post("/items", response_model=ItemResponse)
async def create_item(
item: ItemCreate,
current_user: TokenData = Depends(get_current_user), # Protected route
service: ItemService = Depends(get_item_service)
):
return await service.create_item(item, user_id=current_user.user_id)
Testing Your FastAPI API
FastAPI’s TestClient makes testing straightforward:
# tests/test_items.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine
@pytest.fixture(scope="module")
def client():
Base.metadata.create_all(bind=engine)
with TestClient(app) as c:
yield c
Base.metadata.drop_all(bind=engine)
def test_create_item(client):
response = client.post(
"/api/v1/items",
json={"name": "Test Item", "price": 29.99}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Item"
assert data["price"] == 29.99
assert "id" in data
def test_get_item(client):
# First create an item
create_response = client.post(
"/api/v1/items",
json={"name": "Get Test", "price": 19.99}
)
item_id = create_response.json()["id"]
# Then retrieve it
response = client.get(f"/api/v1/items/{item_id}")
assert response.status_code == 200
assert response.json()["name"] == "Get Test"
def test_get_nonexistent_item(client):
response = client.get("/api/v1/items/99999")
assert response.status_code == 404
def test_validation_error(client):
response = client.post(
"/api/v1/items",
json={"name": "", "price": -10} # Invalid data
)
assert response.status_code == 422
Running Your API
Start your FastAPI server using Uvicorn:
# Development with auto-reload
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Production with multiple workers
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
Access your API at:
http://127.0.0.1:8000– API roothttp://127.0.0.1:8000/docs– Swagger UI documentationhttp://127.0.0.1:8000/redoc– ReDoc documentation
Deployment Considerations
When deploying your FastAPI application to production, consider these factors:
Use Gunicorn with Uvicorn Workers
pip install gunicorn
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
Docker Deployment
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Production Checklist
- Set
DEBUG=Falsein production - Use environment variables for all secrets
- Configure proper CORS origins
- Set up database connection pooling
- Add rate limiting middleware
- Configure logging and monitoring
Conclusion
Building a REST API in Python using FastAPI provides an excellent developer experience with modern features like automatic documentation, type validation, and async support. The framework’s design encourages clean architecture through dependency injection and clear separation of concerns.
Start with the basic structure shown here, then expand as your requirements grow. FastAPI scales well from simple microservices to complex applications serving millions of requests.
For related backend topics, explore our guides on Building a Chat Server with Node.js and Async/Await in JavaScript for comparative approaches to async API development.
2 Comments