FastAPI Development Workflow
Setting up an efficient development workflow can significantly improve your productivity and code quality when building FastAPI applications. This guide will walk you through a recommended workflow for FastAPI projects, from initial setup to development, testing, and deployment.
Introduction
A development workflow is a systematic approach to writing, testing, and deploying code. With FastAPI, having a well-defined workflow helps you leverage its speed and simplicity while maintaining high-quality code standards. Whether you're working on a small personal project or a large team effort, following a structured workflow will save you time and reduce errors.
Setting Up Your FastAPI Project
1. Project Structure
A good project structure helps maintain organization as your application grows. Here's a recommended structure for FastAPI projects:
my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application instance
│ ├── api/ # API endpoints
│ │ ├── __init__.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ ├── items.py
│ │ │ └── users.py
│ ├── core/ # Config, security, etc.
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ ├── db/ # Database models and utils
│ │ ├── __init__.py
│ │ ├── database.py
│ │ └── models.py
│ ├── schemas/ # Pydantic models
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ ├── services/ # Business logic
│ │ ├── __init__.py
│ │ ├── item_service.py
│ │ └── user_service.py
│ └── utils/ # Utility functions
│ ├── __init__.py
│ └── helpers.py
├── tests/ # Tests directory
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_api/
│ └── test_services/
├── .env # Environment variables
├── .gitignore
├── requirements.txt
└── README.md
2. Creating a Virtual Environment
Always use a virtual environment to isolate your project dependencies:
# Create a virtual environment
python -m venv venv
# Activate it (Windows)
venv\Scripts\activate
# Activate it (Mac/Linux)
source venv/bin/activate
# Install FastAPI and dependencies
pip install fastapi uvicorn sqlalchemy pytest
pip freeze > requirements.txt
3. Setting Up Your main.py
File
Here's a basic main.py
file to get started:
from fastapi import FastAPI
from app.api.routes import items, users
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION
)
# Include routers
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(items.router, prefix="/items", tags=["items"])
@app.get("/", tags=["health"])
async def health_check():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
Development Workflow Steps
1. Define Schemas with Pydantic
Always start by defining your data models with Pydantic before implementing routes:
# app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field
class UserBase(BaseModel):
email: EmailStr
full_name: str = Field(..., min_length=2, max_length=50)
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserResponse(UserBase):
id: int
class Config:
orm_mode = True
2. Implement Database Models
Next, create your database models:
# app/db/models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
full_name = Column(String)
hashed_password = Column(String)
3. Create Business Logic in Services
Separate your business logic into service files:
# app/services/user_service.py
from sqlalchemy.orm import Session
from app.db.models import User
from app.schemas.user import UserCreate
from app.core.security import get_password_hash
def create_user(db: Session, user: UserCreate):
db_user = User(
email=user.email,
full_name=user.full_name,
hashed_password=get_password_hash(user.password)
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def get_user(db: Session, user_id: int):
return db.query(User).filter(User.id == user_id).first()
4. Implement API Routes
Implement your API endpoints in router files:
# app/api/routes/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.db.database import get_db
from app.schemas.user import UserCreate, UserResponse
from app.services import user_service
router = APIRouter()
@router.post("/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = user_service.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return user_service.create_user(db=db, user=user)
@router.get("/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = user_service.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
5. Test-Driven Development
Write tests before or alongside your implementation:
# tests/test_api/test_users.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_user():
response = client.post(
"/users/",
json={"email": "[email protected]", "full_name": "Test User", "password": "secret123"},
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "[email protected]"
assert "id" in data
def test_read_user():
# First create a user
create_response = client.post(
"/users/",
json={"email": "[email protected]", "full_name": "Read User", "password": "secret123"},
)
user_id = create_response.json()["id"]
# Then read the user
response = client.get(f"/users/{user_id}")
assert response.status_code == 200
data = response.json()
assert data["email"] == "[email protected]"
assert data["full_name"] == "Read User"
Local Development Tools
Using Uvicorn for Development
The uvicorn
server with auto-reload makes development easier:
# Run from project root
uvicorn app.main:app --reload --port 8000
Using Environment Variables
For configuration, use environment variables with .env
files:
# app/core/config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "FastAPI App"
PROJECT_DESCRIPTION: str = "FastAPI application with best practices"
VERSION: str = "0.1.0"
DATABASE_URL: str = "sqlite:///./app.db"
class Config:
env_file = ".env"
settings = Settings()
And your .env
file:
PROJECT_NAME=My Awesome FastAPI App
DATABASE_URL=postgresql://user:password@localhost:5432/db_name
Database Migrations with Alembic
When your models change, you need to update your database schema:
# Install alembic
pip install alembic
# Initialize alembic
alembic init migrations
# Create a migration
alembic revision --autogenerate -m "Create users table"
# Run migrations
alembic upgrade head
Continuous Integration
Set up GitHub Actions for CI:
# .github/workflows/test.yaml
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest
Deployment Options
Docker Deployment
Create a Dockerfile
for your application:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
And a docker-compose.yml
file for local development:
version: '3'
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app
depends_on:
- db
db:
image: postgres:13
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Cloud Deployment
FastAPI applications can be deployed to:
- Heroku - Simple deployment with buildpacks or Docker
- AWS Lambda with API Gateway - Using Mangum
- Google Cloud Run - Container-based deployment
- Azure App Service - Container or Python-based deployment
Real-World Example
Let's put everything together with a small but complete Todo API:
First, our schemas:
# app/schemas/todo.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class TodoBase(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
is_completed: bool = False
class TodoCreate(TodoBase):
pass
class TodoResponse(TodoBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
Our database model:
# app/db/models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
from sqlalchemy.sql import func
from app.db.database import Base
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(Text, nullable=True)
is_completed = Column(Boolean, default=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, onupdate=func.now())
Our service layer:
# app/services/todo_service.py
from sqlalchemy.orm import Session
from app.db.models import Todo
from app.schemas.todo import TodoCreate
from typing import List
def create_todo(db: Session, todo: TodoCreate):
db_todo = Todo(**todo.dict())
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
def get_todo(db: Session, todo_id: int):
return db.query(Todo).filter(Todo.id == todo_id).first()
def get_todos(db: Session, skip: int = 0, limit: int = 100):
return db.query(Todo).offset(skip).limit(limit).all()
def update_todo(db: Session, todo_id: int, todo: TodoCreate):
db_todo = get_todo(db, todo_id)
if db_todo:
for key, value in todo.dict().items():
setattr(db_todo, key, value)
db.commit()
db.refresh(db_todo)
return db_todo
def delete_todo(db: Session, todo_id: int):
db_todo = get_todo(db, todo_id)
if db_todo:
db.delete(db_todo)
db.commit()
return db_todo
And finally our router:
# app/api/routes/todos.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from app.db.database import get_db
from app.schemas.todo import TodoCreate, TodoResponse
from app.services import todo_service
router = APIRouter()
@router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
return todo_service.create_todo(db=db, todo=todo)
@router.get("/{todo_id}", response_model=TodoResponse)
def read_todo(todo_id: int, db: Session = Depends(get_db)):
db_todo = todo_service.get_todo(db, todo_id=todo_id)
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
@router.get("/", response_model=List[TodoResponse])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
todos = todo_service.get_todos(db, skip=skip, limit=limit)
return todos
@router.put("/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
db_todo = todo_service.update_todo(db, todo_id=todo_id, todo=todo)
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
@router.delete("/{todo_id}", response_model=TodoResponse)
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
db_todo = todo_service.delete_todo(db, todo_id=todo_id)
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
return db_todo
Summary
A well-structured FastAPI development workflow follows these key steps:
- Setup a clear project structure to keep your code organized
- Define schemas first using Pydantic models
- Create database models that represent your data storage
- Implement business logic in service layers
- Build API endpoints using FastAPI routers
- Write tests to verify functionality
- Use local development tools like Uvicorn with reload
- Set up CI/CD for automated testing and deployment
- Deploy using containers or cloud services
By following this workflow, you'll create maintainable, testable, and robust FastAPI applications.
Additional Resources
- FastAPI Documentation
- SQLAlchemy Documentation
- Pydantic Documentation
- TestDriven.io FastAPI Course
- Docker Documentation
Exercises
- Basic Project Setup - Create a new FastAPI project following the recommended structure.
- Todo API Implementation - Implement the Todo API from the example above.
- User Authentication - Add JWT authentication to secure the API endpoints.
- Docker Deployment - Create a Docker setup for your FastAPI application.
- CI/CD Pipeline - Set up a GitHub Actions workflow for continuous integration.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)