FastAPI Project Setup
Quick reference for initializing FastAPI backend projects with modern tooling (UV, SQLModel, pytest).
Quick Start
- Initialize Backend with UV
cd backend
Initialize Python project with UV
uv init --name backend --python 3.13
Create project structure
mkdir -p src/models src/routers src/services src/middleware src/schemas src/utils tests alembic
Create init.py files
touch src/init.py touch src/models/init.py touch src/routers/init.py touch src/services/init.py touch src/middleware/init.py touch src/schemas/init.py touch src/utils/init.py touch tests/init.py
- Install Dependencies
Core dependencies
uv add fastapi[all] uv add sqlmodel uv add psycopg2-binary uv add python-jose[cryptography] uv add passlib[bcrypt] uv add python-dotenv uv add alembic
Development dependencies
uv add --dev pytest uv add --dev pytest-cov uv add --dev httpx uv add --dev pytest-asyncio
- Create Basic FastAPI App
Create src/main.py :
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import os from dotenv import load_dotenv
load_dotenv()
app = FastAPI( title="Todo API", description="RESTful API for Todo Web Application - Phase 2", version="1.0.0" )
CORS configuration
origins = [ os.getenv("FRONTEND_URL", "http://localhost:3000"), ]
app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=[""], allow_headers=[""], )
@app.get("/") async def root(): return {"message": "Todo API is running", "version": "1.0.0"}
@app.get("/health") async def health_check(): return {"status": "healthy"}
if name == "main": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
- Create Configuration
Create src/config.py :
from pydantic_settings import BaseSettings from functools import lru_cache
class Settings(BaseSettings): # Database DATABASE_URL: str
# Authentication
BETTER_AUTH_SECRET: str
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_DAYS: int = 7
# CORS
FRONTEND_URL: str = "http://localhost:3000"
# Environment
ENVIRONMENT: str = "development"
class Config:
env_file = ".env"
case_sensitive = True
@lru_cache() def get_settings(): return Settings()
settings = get_settings()
- Create Environment Variables Template
Create .env.example :
Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
Authentication
BETTER_AUTH_SECRET=your-secret-key-here-min-32-characters
CORS
FRONTEND_URL=http://localhost:3000
Environment
ENVIRONMENT=development
Create actual .env file:
cp .env.example .env
Edit .env with actual values
- Set Up Database Connection
Create src/database.py :
from sqlmodel import create_engine, Session, SQLModel from sqlalchemy.pool import NullPool from src.config import settings
engine = create_engine( settings.DATABASE_URL, echo=settings.ENVIRONMENT == "development", poolclass=NullPool, # Let Neon handle pooling connect_args={ "connect_timeout": 10, "options": "-c timezone=utc" } )
def create_db_and_tables(): """Create all database tables""" SQLModel.metadata.create_all(engine)
def get_session(): """FastAPI dependency for database sessions""" with Session(engine) as session: yield session
- Initialize Alembic for Migrations
Initialize Alembic
alembic init alembic
Edit alembic.ini - comment out the sqlalchemy.url line
It will be set from environment variable
Edit alembic/env.py
Update alembic/env.py :
from logging.config import fileConfig from sqlmodel import SQLModel from sqlalchemy import engine_from_config, pool from alembic import context import os from dotenv import load_dotenv
Load environment variables
load_dotenv()
Import all models so Alembic can detect them
from src.models.task import Task # Import as you create models
Alembic Config object
config = context.config
Set DATABASE_URL from environment
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
Interpret the config file for Python logging
if config.config_file_name is not None: fileConfig(config.config_file_name)
Set target metadata for autogenerate
target_metadata = SQLModel.metadata
... rest of env.py (keep default functions)
- Create Basic Test Setup
Create tests/conftest.py :
import pytest from fastapi.testclient import TestClient from sqlmodel import Session, create_engine, SQLModel from sqlmodel.pool import StaticPool
from src.main import app from src.database import get_session
@pytest.fixture(name="session") def session_fixture(): engine = create_engine( "sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) SQLModel.metadata.create_all(engine) with Session(engine) as session: yield session
@pytest.fixture(name="client") def client_fixture(session: Session): def get_session_override(): return session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app)
yield client
app.dependency_overrides.clear()
Create tests/test_main.py :
def test_root(client): response = client.get("/") assert response.status_code == 200 assert "message" in response.json()
def test_health_check(client): response = client.get("/health") assert response.status_code == 200 assert response.json()["status"] == "healthy"
- Create pyproject.toml
Create or update pyproject.toml :
[project] name = "backend" version = "1.0.0" description = "Todo API Backend - Phase 2" requires-python = ">=3.13" dependencies = [ "fastapi[all]>=0.115.0", "sqlmodel>=0.0.24", "psycopg2-binary>=2.9.9", "python-jose[cryptography]>=3.3.0", "passlib[bcrypt]>=1.7.4", "python-dotenv>=1.0.0", "alembic>=1.13.0", ]
[project.optional-dependencies] dev = [ "pytest>=8.0.0", "pytest-cov>=4.1.0", "httpx>=0.26.0", "pytest-asyncio>=0.23.0", ]
[build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_.py"] python_classes = ["Test"] python_functions = ["test_*"] addopts = "-v --cov=src --cov-report=term-missing"
- Test the Setup
Run the FastAPI server
cd backend uv run uvicorn src.main:app --reload --port 8000
In another terminal, run tests
uv run pytest
Check code coverage
uv run pytest --cov=src --cov-report=html
Verification Checklist
After setup, verify:
-
uv run uvicorn src.main:app --reload starts successfully
-
http://localhost:8000 returns JSON response
-
http://localhost:8000/docs shows Swagger UI
-
http://localhost:8000/health returns healthy status
-
uv run pytest runs and passes
-
.env file exists and has DATABASE_URL
-
All init.py files created
-
Alembic initialized successfully
Next Steps
After basic setup:
-
Create database models in src/models/
-
Create API routers in src/routers/
-
Set up JWT authentication middleware
-
Create first Alembic migration
-
Write comprehensive tests
Troubleshooting
UV not found:
curl -LsSf https://astral.sh/uv/install.sh | sh
Import errors:
-
Ensure all init.py files exist
-
Check PYTHONPATH includes src directory
-
Use uv run python instead of just python
Database connection errors:
-
Verify DATABASE_URL in .env
-
Check Neon database is running
-
Test connection with psql
References
-
FastAPI: https://fastapi.tiangolo.com/
-
SQLModel: https://sqlmodel.tiangolo.com/
-
Alembic: https://alembic.sqlalchemy.org/