FastAPI/
Lesson

Configuration is one of those things that separates a demo from a real application. Your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. needs to know the database URL, secret keys, API tokens, feature flags, and port numbers. Where this information lives and how it gets into your app determines whether your project is deployable, secure, and maintainable. AI gets this wrong more often than almost anything else.

The problem AI creates

Here is what AI typically generates when you ask for a FastAPI app with database access:

# main.py - the AI-generated version
from fastapi import FastAPI
from sqlalchemy import create_engine

app = FastAPI()

DATABASE_URL = "postgresql://admin:password123@localhost:5432/mydb"
JWT_SECRET = "super-secret-key-dont-share"
DEEPSEEK_API_KEY = "sk-abc123def456"

engine = create_engine(DATABASE_URL)

This code works. It also has three critical problems:

  1. Secrets in source code. If this gets pushed to GitHub (and it will, people forget), your database credentials and APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. keys are public. Bots scrape GitHub for exactly these patterns.
  2. No environment separation. The same code runs against your production database in development. One wrong DELETE query and your production data is gone.
  3. No validation. If someone deploys this without setting up the database, it crashes at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary. with a confusing connection error instead of a clear "DATABASE_URL is missing" message at startup.
AI pitfall
AI treats configuration as a solved problem because it "works" in the generated code. It does not think about deployment, security, or team workflows. Every time AI hardcodes a connection string or API key, that is a security vulnerability you need to fix before the code goes anywhere near a repository.
02

Enter pydantic-settings

The pydantic-settings library solves all three problems. It reads configuration from environment variables, validates them at startup, and gives you typed access throughout your app.

pip install pydantic-settings
# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False,
    )

    # Database
    database_url: str
    database_echo: bool = False

    # Auth
    jwt_secret: str
    jwt_algorithm: str = "HS256"
    access_token_expire_minutes: int = 30

    # External APIs
    deepseek_api_key: str

    # App
    app_name: str = "My API"
    debug: bool = False
    allowed_origins: list[str] = ["http://localhost:3000"]

This class does several things at once:

  • Reads from environment variables. database_url reads from DATABASE_URL (case-insensitive by default).
  • Falls back to .env file. If the environment variableWhat is environment variable?A value stored outside your code that configures behavior per deployment, commonly used for secrets like API keys and database URLs. is not set, it reads from the .env file in the project root.
  • Validates at startup. If database_url is missing and has no default, the app crashes immediately with a clear error: ValidationError: database_url field required.
  • Provides type safety. access_token_expire_minutes is always an int. If someone sets it to "thirty", Pydantic catches it.
03

The .env file pattern

Your .env file contains the actual secrets. Your .env.example documents the required variables without real values.

# .env - NEVER commit this
DATABASE_URL=postgresql://admin:realpassword@localhost:5432/mydb
JWT_SECRET=a-real-random-string-here
DEEPSEEK_API_KEY=sk-real-key-here
DEBUG=true
# .env.example - DO commit this
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=change-me-to-a-random-string
DEEPSEEK_API_KEY=your-api-key-here
DEBUG=false

Your .gitignore must include .env:

# .gitignore
.env
.env.local
.env.production
AI pitfall
AI almost never generates a .gitignore that includes .env, and it almost never creates a .env.example file. When reviewing AI-generated projects, check for both immediately. A missing .gitignore entry is a ticking time bomb.
04

Settings as a dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself.

The cleanest way to use settings in FastAPI is as a dependency. This makes your routes testable (you can override settings in tests) and ensures every part of your app gets the same configuration instance.

# app/core/config.py
from functools import lru_cache

@lru_cache
def get_settings() -> Settings:
    return Settings()

The @lru_cache decorator ensures the Settings object is created exactly once and reused for every request. Without it, every request would re-read the .env file and re-validate everything, wasteful and slow.

# app/routers/items.py
from fastapi import APIRouter, Depends
from core.config import Settings, get_settings

router = APIRouter()

@router.get("/debug")
def debug_info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "debug": settings.debug,
    }

In tests, you can override the dependency:

# tests/conftest.py
from core.config import get_settings, Settings

def get_test_settings():
    return Settings(
        database_url="sqlite:///test.db",
        jwt_secret="test-secret",
        deepseek_api_key="test-key",
    )

app.dependency_overrides[get_settings] = get_test_settings
05

Nested settings for complex configuration

As your configuration grows, you can organize it into nested groups:

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseModel):
    url: str
    echo: bool = False
    pool_size: int = 5

class AuthSettings(BaseModel):
    jwt_secret: str
    jwt_algorithm: str = "HS256"
    access_token_expire_minutes: int = 30

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_nested_delimiter="__",
    )

    database: DatabaseSettings
    auth: AuthSettings
    debug: bool = False

With env_nested_delimiter="__", you set nested values like this:

DATABASE__URL=postgresql://localhost/mydb
DATABASE__POOL_SIZE=10
AUTH__JWT_SECRET=my-secret

This keeps your settings organized as the project grows. Instead of 30 flat variables, you get logical groups that mirror your application's structure.

06

Startup validation pattern

The best practice is to validate settings the moment your app starts, not when the first request hits a route that needs them:

# app/main.py
from fastapi import FastAPI
from core.config import get_settings

def create_app() -> FastAPI:
    settings = get_settings()  # Crashes immediately if config is invalid

    app = FastAPI(
        title=settings.app_name,
        debug=settings.debug,
    )

    # Mount routers...
    return app

app = create_app()

If DATABASE_URL is missing, the app fails at startup with a clear Pydantic validation error, not 10 minutes later when someone hits an endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. that needs the database. This is the difference between a helpful error message and a 3 AM debugging sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request..

07

Configuration checklist

When reviewing AI-generated FastAPI code, check for these:

CheckWhat to look for
No hardcoded secretsSearch for = followed by what looks like passwords, keys, or connection strings
.env in .gitignoreMust be present before the first commit
.env.example existsDocuments every required variable
Settings validated at startupApp should fail fast if config is missing
Settings used as dependencyNot imported as a global, enables testing
No secrets in logsprint(settings) should not dump your API keys

This is one area where you cannot trust AI defaults. Configuration security requires human judgment every single time.