Python

How to Build a REST API in Python Using FastAPI

20250409 1359 Building FastAPI REST API Simple Compose 01jrd6prhbf6gtsct81h1q3kq6 1024x683

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 root
  • http://127.0.0.1:8000/docs – Swagger UI documentation
  • http://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=False in 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.