Skip to main content

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:

bash
# 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:

python
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:

python
# 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:

python
# 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:

python
# 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:

python
# 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:

python
# 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:

bash
# Run from project root
uvicorn app.main:app --reload --port 8000

Using Environment Variables

For configuration, use environment variables with .env files:

python
# 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:

bash
# 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:

yaml
# .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:

dockerfile
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:

yaml
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:

  1. Heroku - Simple deployment with buildpacks or Docker
  2. AWS Lambda with API Gateway - Using Mangum
  3. Google Cloud Run - Container-based deployment
  4. 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:

python
# 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:

python
# 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:

python
# 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:

python
# 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:

  1. Setup a clear project structure to keep your code organized
  2. Define schemas first using Pydantic models
  3. Create database models that represent your data storage
  4. Implement business logic in service layers
  5. Build API endpoints using FastAPI routers
  6. Write tests to verify functionality
  7. Use local development tools like Uvicorn with reload
  8. Set up CI/CD for automated testing and deployment
  9. Deploy using containers or cloud services

By following this workflow, you'll create maintainable, testable, and robust FastAPI applications.

Additional Resources

Exercises

  1. Basic Project Setup - Create a new FastAPI project following the recommended structure.
  2. Todo API Implementation - Implement the Todo API from the example above.
  3. User Authentication - Add JWT authentication to secure the API endpoints.
  4. Docker Deployment - Create a Docker setup for your FastAPI application.
  5. 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! :)