FastAPI in Practice: Build Reusable Dependencies for Auth, Settings, and Database Sessions
FastAPI becomes much easier to maintain when you stop putting setup logic directly inside route functions. Junior developers often start with routes that open database connections, read environment variables, validate tokens, and process request data all in the same place. That works for a small prototype, but it gets messy as soon as the app grows.
This guide shows how to use FastAPI dependencies to keep your API clean. We will build a small task API with three reusable pieces:
get_settings()for application configuration.get_db()for database sessions.get_current_user()for simple token-based authentication.
The examples use SQLite so you can run them locally without extra infrastructure, but the same structure works with PostgreSQL or MySQL.
Project Setup
Create a new folder and install the required packages:
mkdir fastapi-dependencies-demo cd fastapi-dependencies-demo python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install fastapi uvicorn sqlalchemy pydantic-settings
Create this simple project structure:
app/ main.py config.py database.py models.py dependencies.py
Step 1: Add Application Settings
Configuration should not be hardcoded inside your route handlers. Instead, use a settings object that reads from environment variables. This keeps your app portable between local development, staging, and production.
Create app/config.py:
from functools import lru_cache from pydantic_settings import BaseSettings class Settings(BaseSettings): app_name: str = "Task API" debug: bool = True database_url: str = "sqlite:///./tasks.db" api_token: str = "dev-secret-token" class Config: env_file = ".env" @lru_cache def get_settings() -> Settings: return Settings()
The @lru_cache decorator makes sure the settings object is created only once. Without it, FastAPI could recreate settings every time the dependency is called.
You can override the defaults later by creating a .env file:
APP_NAME="Production Task API" DEBUG=false DATABASE_URL="sqlite:///./tasks.db" API_TOKEN="change-this-token"
Step 2: Create the Database Session Dependency
Next, create a SQLAlchemy database setup. The important part is the get_db() function. It opens a session, gives it to the route, and closes it after the request finishes.
Create app/database.py:
from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, sessionmaker from app.config import get_settings settings = get_settings() engine = create_engine( settings.database_url, connect_args={"check_same_thread": False} ) SessionLocal = sessionmaker( autocommit=False, autoflush=False, bind=engine ) class Base(DeclarativeBase): pass def get_db(): db = SessionLocal() try: yield db finally: db.close()
The yield keyword is what makes this dependency special. Code before yield runs before the route handler. Code after yield runs after the response is prepared. This pattern is perfect for database sessions, file handles, and other resources that need cleanup.
Step 3: Define a Simple Model
Create app/models.py:
from sqlalchemy import Boolean, Integer, String from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class Task(Base): __tablename__ = "tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) title: Mapped[str] = mapped_column(String(200), nullable=False) completed: Mapped[bool] = mapped_column(Boolean, default=False)
This model represents a task with an ID, title, and completion status.
Step 4: Add an Authentication Dependency
Authentication is another good use case for dependencies. Instead of checking headers manually in every route, create one reusable function.
Create app/dependencies.py:
from fastapi import Depends, Header, HTTPException, status from sqlalchemy.orm import Session from app.config import Settings, get_settings from app.database import get_db def get_current_user( x_api_token: str | None = Header(default=None), settings: Settings = Depends(get_settings) ) -> dict: if x_api_token != settings.api_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or missing API token" ) return { "username": "demo-user", "role": "developer" } def get_task_dependencies( db: Session = Depends(get_db), user: dict = Depends(get_current_user) ) -> dict: return { "db": db, "user": user }
This example uses an X-API-Token header. In production, you might use OAuth2, JWTs, or session cookies, but the dependency structure would stay similar.
The get_task_dependencies() function also shows that dependencies can depend on other dependencies. This is useful when several routes always need the same combination of resources.
Step 5: Build the API Routes
Now create app/main.py:
from fastapi import Depends, FastAPI, HTTPException, status from pydantic import BaseModel from sqlalchemy.orm import Session from app.config import Settings, get_settings from app.database import Base, engine, get_db from app.dependencies import get_current_user from app.models import Task Base.metadata.create_all(bind=engine) app = FastAPI() class TaskCreate(BaseModel): title: str class TaskResponse(BaseModel): id: int title: str completed: bool model_config = { "from_attributes": True } @app.get("/") def read_root(settings: Settings = Depends(get_settings)): return { "app": settings.app_name, "debug": settings.debug } @app.post( "/tasks", response_model=TaskResponse, status_code=status.HTTP_201_CREATED ) def create_task( payload: TaskCreate, db: Session = Depends(get_db), user: dict = Depends(get_current_user) ): task = Task(title=payload.title) db.add(task) db.commit() db.refresh(task) return task @app.get("/tasks", response_model=list[TaskResponse]) def list_tasks( db: Session = Depends(get_db), user: dict = Depends(get_current_user) ): return db.query(Task).order_by(Task.id.desc()).all() @app.patch("/tasks/{task_id}/complete", response_model=TaskResponse) def complete_task( task_id: int, db: Session = Depends(get_db), user: dict = Depends(get_current_user) ): task = db.query(Task).filter(Task.id == task_id).first() if task is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Task not found" ) task.completed = True db.commit() db.refresh(task) return task
Each protected route receives db and user through dependencies. The route function only focuses on business logic: create a task, list tasks, or mark a task as complete.
Step 6: Run and Test the API
Start the development server:
uvicorn app.main:app --reload
Test the root route:
curl http://127.0.0.1:8000/
Create a task with the token header:
curl -X POST http://127.0.0.1:8000/tasks \ -H "Content-Type: application/json" \ -H "X-API-Token: dev-secret-token" \ -d '{"title": "Learn FastAPI dependencies"}'
List tasks:
curl http://127.0.0.1:8000/tasks \ -H "X-API-Token: dev-secret-token"
Mark a task as complete:
curl -X PATCH http://127.0.0.1:8000/tasks/1/complete \ -H "X-API-Token: dev-secret-token"
If you omit the token, the API returns a 401 Unauthorized response:
{ "detail": "Invalid or missing API token" }
Why This Pattern Is Better
Dependencies give your FastAPI application a clean structure. Instead of repeating the same setup code across many endpoints, you define it once and inject it where needed.
- Cleaner routes: route functions stay focused on request handling and business rules.
- Less duplication: authentication, settings, and database setup live in one place.
- Easier testing: FastAPI lets you override dependencies during tests.
- Safer resource handling: sessions and connections can be closed reliably after each request.
Testing with Dependency Overrides
One major benefit of this approach is testability. For example, you can replace the real authentication dependency with a fake one in tests.
from fastapi.testclient import TestClient from app.main import app from app.dependencies import get_current_user def fake_user(): return { "username": "test-user", "role": "tester" } app.dependency_overrides[get_current_user] = fake_user client = TestClient(app) def test_create_task_without_real_token(): response = client.post( "/tasks", json={"title": "Test task"} ) assert response.status_code == 201 assert response.json()["title"] == "Test task"
This lets your tests focus on route behavior instead of real authentication. For database tests, you can also override get_db() with a temporary test database session.
Common Mistakes to Avoid
- Creating database sessions globally per request: use a dependency with
yieldso sessions close properly. - Reading environment variables inside every route: use a cached settings dependency.
- Mixing authentication with business logic: keep user lookup and permission checks in dependencies.
- Overusing dependencies: not every helper function needs to be a FastAPI dependency. Use them when request context, cleanup, or override behavior matters.
Conclusion
FastAPI dependencies are more than a convenience feature. They are one of the best ways to organize a real application. By moving configuration, database sessions, and authentication into reusable dependencies, your routes become smaller, easier to read, and easier to test.
For junior and mid-level developers, the key habit is simple: when several routes repeat the same setup code, turn that setup into a dependency. Start with get_settings(), get_db(), and get_current_user(). Those three patterns will carry most FastAPI projects much further than a single-file prototype.
Leave a Reply