FastAPI Tutorial Part 20: Building a Production-Ready API
Build a complete production-ready FastAPI application. Combine all concepts into a real-world e-commerce API with authentication, database, and deployment.
Moshiour Rahman
Advertisement
Project Overview
We’ll build a complete e-commerce API with:
- User authentication (JWT)
- Product management
- Order processing
- Admin panel
- Full test coverage
Project Structure
ecommerce-api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── product.py
│ │ └── order.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── product.py
│ │ └── order.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── users.py
│ │ ├── products.py
│ │ └── orders.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ └── email.py
│ └── utils/
│ ├── __init__.py
│ └── security.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_products.py
│ └── test_orders.py
├── alembic/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── .env.example
Configuration
# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
app_name: str = "E-Commerce API"
debug: bool = False
database_url: str
secret_key: str
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
class Config:
env_file = ".env"
@lru_cache()
def get_settings():
return Settings()
Database Models
# app/models/user.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from ..database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
orders = relationship("Order", back_populates="user")
# app/models/product.py
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
name = Column(String, index=True)
description = Column(String)
price = Column(Float)
stock = Column(Integer, default=0)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# app/models/order.py
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
total = Column(Float)
status = Column(String, default="pending")
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order")
Authentication
# app/routers/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from ..database import get_db
from ..schemas.user import UserCreate, UserResponse, Token
from ..services.auth import authenticate_user, create_access_token, get_password_hash
from ..models.user import User
router = APIRouter(prefix="/auth", tags=["Authentication"])
@router.post("/register", response_model=UserResponse, status_code=201)
def register(user: UserCreate, db: Session = Depends(get_db)):
if db.query(User).filter(User.email == user.email).first():
raise HTTPException(status_code=400, detail="Email already registered")
db_user = User(
email=user.email,
username=user.username,
hashed_password=get_password_hash(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token = create_access_token(data={"sub": str(user.id)})
return Token(access_token=access_token, token_type="bearer")
Products Router
# app/routers/products.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from ..database import get_db
from ..models.product import Product
from ..schemas.product import ProductCreate, ProductResponse, ProductList
from ..services.auth import get_current_user, require_admin
router = APIRouter(prefix="/products", tags=["Products"])
@router.get("/", response_model=ProductList)
def list_products(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
search: Optional[str] = None,
db: Session = Depends(get_db)
):
query = db.query(Product).filter(Product.is_active == True)
if search:
query = query.filter(Product.name.ilike(f"%{search}%"))
total = query.count()
products = query.offset(skip).limit(limit).all()
return ProductList(items=products, total=total)
@router.post("/", response_model=ProductResponse, status_code=201)
def create_product(
product: ProductCreate,
db: Session = Depends(get_db),
admin = Depends(require_admin)
):
db_product = Product(**product.model_dump())
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
@router.get("/{product_id}", response_model=ProductResponse)
def get_product(product_id: int, db: Session = Depends(get_db)):
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
Orders Router
# app/routers/orders.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..database import get_db
from ..models.order import Order, OrderItem
from ..models.product import Product
from ..schemas.order import OrderCreate, OrderResponse
from ..services.auth import get_current_user
router = APIRouter(prefix="/orders", tags=["Orders"])
@router.post("/", response_model=OrderResponse, status_code=201)
def create_order(
order: OrderCreate,
db: Session = Depends(get_db),
user = Depends(get_current_user)
):
total = 0
order_items = []
for item in order.items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if not product:
raise HTTPException(status_code=400, detail=f"Product {item.product_id} not found")
if product.stock < item.quantity:
raise HTTPException(status_code=400, detail=f"Insufficient stock for {product.name}")
product.stock -= item.quantity
total += product.price * item.quantity
order_items.append(OrderItem(
product_id=product.id,
quantity=item.quantity,
price=product.price
))
db_order = Order(user_id=user.id, total=total, items=order_items)
db.add(db_order)
db.commit()
db.refresh(db_order)
return db_order
@router.get("/my-orders")
def get_my_orders(
db: Session = Depends(get_db),
user = Depends(get_current_user)
):
return db.query(Order).filter(Order.user_id == user.id).all()
Main Application
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from .database import engine, Base
from .routers import auth, users, products, orders
from .config import get_settings
settings = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
Base.metadata.create_all(bind=engine)
yield
app = FastAPI(
title=settings.app_name,
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(products.router)
app.include_router(orders.router)
@app.get("/health")
def health_check():
return {"status": "healthy"}
Docker Configuration
# 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"]
# docker-compose.yml
version: "3.8"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db/ecommerce
- SECRET_KEY=your-secret-key
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=ecommerce
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Running the Project
# Development
uvicorn app.main:app --reload
# Production with Docker
docker-compose up --build
# Run tests
pytest tests/ -v
Series Complete!
Congratulations on completing the FastAPI tutorial series! You’ve learned:
| Part | Topic |
|---|---|
| 1-4 | Foundations: Setup, Parameters, Pydantic, Responses |
| 5-7 | Data: Dependencies, Database, CRUD |
| 8-10 | Security: Errors, JWT Auth, RBAC |
| 11-14 | Advanced: Tasks, WebSockets, Middleware, Files |
| 15-19 | Production: Testing, Docker, Performance, Security, Docs |
| 20 | Full Project |
Next Steps
- Deploy to cloud (AWS, GCP, Azure)
- Add monitoring (Prometheus, Grafana)
- Implement GraphQL endpoint
- Add real-time notifications
- Build admin dashboard
Happy coding with FastAPI!
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
FastAPI Tutorial Part 5: Dependency Injection - Share Logic Across Endpoints
Master FastAPI dependency injection for clean, reusable code. Learn database sessions, authentication, pagination, and complex dependency chains.
PythonFastAPI Tutorial Part 8: Error Handling and Exceptions
Master error handling in FastAPI. Learn custom exceptions, global handlers, validation errors, and building user-friendly error responses.
PythonFastAPI Tutorial Part 1: Introduction and Setup - Build Modern Python APIs
Start your FastAPI journey with this comprehensive guide. Learn installation, create your first API, understand async Python, and explore automatic documentation.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.