I've been tinkering with FastAPI and have found a couple of useful repos. However there are so many different tools and styles in use, I found it hard to bootstrap and start my projects. This guide provides a step-by-step process for scaffolding a FastAPI project using fastapi-users for authentication and authorization, PostgreSQL as the database, Alembic for migrations, and uv for package management. I will try to build this out over time as I incorporate additional tools/techniques. I'm learning a lot from the zhanymkanov/fastapi-best-practices repository. ππ§π
Ensure that Python 3.7+ is installed, then globally install uv:
pip install uvVerify the installation:
uv --versionCreate a new directory for the project and initialize it using uv:
mkdir fastapi-bootstrap
cd fastapi-bootstrap
uv initThis command creates a pyproject.toml file for managing dependencies. ππ¦π§
Add the following dependencies using uv:
uv add fastapi uvicorn psycopg2-binary python-dotenv alembic passlib bcrypt
uv add fastapi-users --extra sqlalchemyuv will install and pin the specified versions of the packages while updating pyproject.toml automatically. πβ¨π
Hereβs the recommended directory structure based on best practices:
fastapi-project/
βββ app/
β βββ __init__.py
β βββ main.py
β βββ core/
β β βββ __init__.py
β β βββ config.py
β β βββ constants.py
β β βββ database.py
β β βββ exceptions.py
β β βββ security.py
β βββ users/
β β βββ __init__.py
β β βββ auth.py
β β βββ manager.py
β β βββ models.py
β β βββ schemas.py
β βββ api/
β βββ __init__.py
β βββ routes.py
βββ alembic/
β βββ versions/
βββ alembic.ini
βββ .env
Create a .env file to store environment variables:
DATABASE_URL=postgresql://username:password@localhost:5432/mydatabase
SECRET_KEY=your_secret_key_herefrom typing import Any
from collections.abc import AsyncGenerator
from sqlalchemy import (
CursorResult,
Insert,
MetaData,
Select,
Update
)
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession, async_sessionmaker, create_async_engine
from .config import settings
from .constants import DB_NAMING_CONVENTION
Base: DeclarativeMeta = declarative_base()
DATABASE_URL = str(settings.DATABASE_ASYNC_URL)
engine = create_async_engine(
DATABASE_URL,
pool_size = settings.DATABASE_POOL_SIZE,
pool_recycle = settings.DATABASE_POOL_TTL,
pool_pre_ping = settings.DATABASE_POOL_PRE_PING
)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
metadata = MetaData(naming_convention=DB_NAMING_CONVENTION)
async def create_db_and_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_sfrom fastapi_users import schemas
class UserRead(schemas.BaseUser[int]):
pass
class UserCreate(schemas.BaseUserCreate):
pass
class UserUpdate(schemas.BaseUserUpdate):
passfrom fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from ..core.database import Base, get_async_session
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(session, User)from fastapi_users import BaseUserManager, IntegerIDMixin
from .schemas import UserCreate
from ..db.models import User
from ..db.base import SessionLocal
import os
SECRET_KEY = os.getenv("SECRET_KEY")
class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
reset_password_token_secret = SECRET_KEY
verification_token_secret = SECRET_KEY
async def on_after_register(self, user: User, request=None):
print(f"User {user.email} has registered.")
def get_user_manager():
db = SessionLocal()
yield UserManager(db)from fastapi import Depends
from fastapi_users import FastAPIUsers
from fastapi_users.authentication import BearerTransport, JWTStrategy, AuthenticationBackend
from .schemas import UserRead, UserCreate, UserUpdate
from .manager import get_user_manager
from ..db.models import User
SECRET_KEY = os.getenv("SECRET_KEY")
def get_jwt_strategy() -> JWTStrategy:
return JWTStrategy(secret=SECRET_KEY, lifetime_seconds=3600)
auth_backend = AuthenticationBackend(
name="jwt",
transport=BearerTransport(tokenUrl="auth/jwt/login"),
get_strategy=get_jwt_strategy,
)
fastapi_users = FastAPIUsers[User, int](
get_user_manager,
[auth_backend],
)
current_active_user = fastapi_users.current_user(active=True)from fastapi import APIRouter
from ..users.auth import fastapi_users, auth_backend
from ..users.schemas import UserRead, UserCreate, UserUpdate
router = APIRouter()
# User routes
router.include_router(
fastapi_users.get_auth_router(auth_backend),
prefix="/auth/jwt",
tags=["auth"],
)
router.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)from fastapi import FastAPI
from .api.routes import router as api_router
from .db.base import Base, engine
app = FastAPI()
# Create database tables
Base.metadata.create_all(bind=engine)
# Include API router
app.include_router(api_router)alembic init alembicEdit alembic.ini and set the sqlalchemy.url to the database URL from .env. Additionally, update env.py to include the target_metadata from your models by importing the Base class and setting target_metadata = Base.metadata. This ensures Alembic can detect schema changes correctly during migrations:
sqlalchemy.url = postgresql://username:password@localhost:5432/mydatabasefrom app.db.base import Base
target_metadata = Base.metadataalembic revision --autogenerate -m "Initial migration"
alembic upgrade headStart the FastAPI application using uv:
uv run app.main:app --reloadVisit http://127.0.0.1:8000/docs to test the API using the automatically generated interactive documentation. ππβ¨
uv add <package-name>uv updateuv remove <package-name>