Free Ebook cover FastAPI for Beginners: Build a Production-Ready REST API

FastAPI for Beginners: Build a Production-Ready REST API

New course

14 pages

Configuration Management and Environment Separation

CapĂ­tulo 12

Estimated reading time: 7 minutes

+ Exercise

Why configuration management matters

A production-ready API needs different behavior depending on where it runs: local development, automated tests, staging, and production. Configuration management is the practice of keeping those differences explicit, validated, and safe. In FastAPI projects, the most common approach is: (1) store configuration in environment variables, (2) load and validate them through a settings class, and (3) keep secrets out of source control.

This chapter focuses on separating environments and managing settings such as database URLs, secrets, debug flags, and CORS. The goal is to make it easy to switch environments without changing code.

Environment separation: what changes across dev/test/prod

Typical differences between environments include:

  • Database URL: local Postgres vs. managed cloud database; test database vs. production database.
  • Secrets: JWT signing keys, API keys, encryption keys (never hard-coded).
  • Debug behavior: verbose logging and error details in development; minimal details in production.
  • CORS: permissive local origins vs. strict production origins.
  • Feature flags: enabling/disabling experimental features.

Instead of branching logic everywhere, centralize these values in a single settings object that the rest of the app reads.

Recommended project structure for configuration

A simple, maintainable structure:

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

app/  config.py  main.py  ...  

Where app/config.py defines a settings class and app/main.py loads it once and wires it into the application.

Step-by-step: create a validated settings class

1) Define settings with Pydantic Settings

Use pydantic-settings (Pydantic v2) to load environment variables and validate them. This gives you type safety, defaults, and clear errors when required values are missing.

from typing import List, Optional  from pydantic import AnyUrl, Field, SecretStr  from pydantic_settings import BaseSettings, SettingsConfigDict  class Settings(BaseSettings):      model_config = SettingsConfigDict(          env_file=('.env',),          env_file_encoding='utf-8',          extra='ignore',      )      # --- Core environment flags ---      environment: str = Field(default='development', description='development|testing|production')      debug: bool = False      # --- Database ---      database_url: AnyUrl = Field(..., description='Database connection URL')      # --- Secrets ---      secret_key: SecretStr = Field(..., min_length=32, description='App secret used for signing/encryption')      # --- CORS ---      cors_allow_origins: List[str] = Field(default_factory=list)      cors_allow_credentials: bool = True      cors_allow_methods: List[str] = Field(default_factory=lambda: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])      cors_allow_headers: List[str] = Field(default_factory=lambda: ['Authorization', 'Content-Type'])  settings = Settings()

What this gives you:

  • database_url and secret_key are required (Field(...)). If missing, the app fails fast on startup with a clear validation error.
  • SecretStr prevents accidental printing of secret values (it masks them in logs and repr).
  • env_file allows local development to use a .env file, while production can rely on real environment variables.

2) Add safe defaults and environment-specific constraints

Some values should have safe defaults (like debug=False), while others should be mandatory in production (like strict CORS). You can enforce this with custom validation.

from pydantic import model_validator  class Settings(BaseSettings):      model_config = SettingsConfigDict(env_file=('.env',), env_file_encoding='utf-8', extra='ignore')      environment: str = 'development'      debug: bool = False      database_url: AnyUrl = Field(...)      secret_key: SecretStr = Field(..., min_length=32)      cors_allow_origins: List[str] = Field(default_factory=list)      @model_validator(mode='after')      def validate_environment_rules(self):          env = self.environment.lower()          if env not in {'development', 'testing', 'production'}:              raise ValueError('environment must be development, testing, or production')          # In production, require explicit CORS origins (avoid '*')          if env == 'production':              if not self.cors_allow_origins:                  raise ValueError('cors_allow_origins must be set in production')              if '*' in self.cors_allow_origins:                  raise ValueError("Do not use '*' in cors_allow_origins in production")          # Debug should not be enabled in production          if env == 'production' and self.debug:              raise ValueError('debug must be False in production')          return self

This keeps your application from starting with insecure settings in production-like environments.

3) Support common environment variable naming

Many teams prefer uppercase environment variables (e.g., DATABASE_URL). Pydantic can map them automatically. You can also add a prefix to avoid collisions.

class Settings(BaseSettings):      model_config = SettingsConfigDict(          env_file=('.env',),          env_prefix='APP_',          extra='ignore',      )      database_url: AnyUrl = Field(...)      secret_key: SecretStr = Field(..., min_length=32)      debug: bool = False

With env_prefix='APP_', the environment variables become APP_DATABASE_URL, APP_SECRET_KEY, etc.

Step-by-step: wiring settings into FastAPI

1) Load settings once

Load settings at import time (or in a startup hook) so misconfiguration fails early. A common pattern is to expose a get_settings() function that returns a cached instance.

from functools import lru_cache  from .config import Settings  @lru_cache  def get_settings() -> Settings:      return Settings()

2) Apply CORS settings from configuration

Use the settings values to configure middleware. Keep the middleware configuration centralized so it changes per environment without code edits.

from fastapi import FastAPI  from fastapi.middleware.cors import CORSMiddleware  from .settings_loader import get_settings  def create_app() -> FastAPI:      settings = get_settings()      app = FastAPI(debug=settings.debug)      if settings.cors_allow_origins:          app.add_middleware(              CORSMiddleware,              allow_origins=settings.cors_allow_origins,              allow_credentials=settings.cors_allow_credentials,              allow_methods=settings.cors_allow_methods,              allow_headers=settings.cors_allow_headers,          )      return app  app = create_app()

In development you might set cors_allow_origins to ["http://localhost:5173"]. In production, you set it to your real frontend origin(s).

Database URLs, secrets, and debug flags: practical patterns

Database URL patterns

Keep a single database_url setting and switch it per environment. Examples:

  • Development: postgresql+psycopg://postgres:postgres@localhost:5432/app_dev
  • Testing: postgresql+psycopg://postgres:postgres@localhost:5432/app_test
  • Production: postgresql+psycopg://user:pass@db.example.com:5432/app (provided by your platform)

Do not embed production credentials in code or committed files. The production URL should come from the deployment environment.

Secrets: never commit them

Use environment variables for secrets. For local development, store them in a .env file that is ignored by Git.

# .gitignore  .env  .env.*

Create a committed template file instead, so teammates know what to set:

# .env.example (commit this)  APP_ENVIRONMENT=development  APP_DEBUG=true  APP_DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/app_dev  APP_SECRET_KEY=change-me-to-a-long-random-string  APP_CORS_ALLOW_ORIGINS=["http://localhost:5173"]

Then each developer creates their own .env locally by copying .env.example and replacing values.

Debug flags: safe defaults

Default debug to False and only enable it explicitly in development. When debug=True, you may expose stack traces and internal details; your settings validation should prevent this in production.

CORS settings: strict in production, convenient in development

CORS controls which browser-based frontends can call your API. A good approach:

  • Development: allow your local frontend origin(s).
  • Production: allow only the real domain(s) that host your frontend.
  • Avoid * in production, especially when credentials (cookies/authorization headers) are involved.

Because CORS values are configuration, you can deploy the same code to multiple environments and only change APP_CORS_ALLOW_ORIGINS.

Validation of required settings and fail-fast behavior

Failing fast means the application refuses to start if required configuration is missing or unsafe. This is preferable to starting with partial configuration and failing later under load.

Example: if APP_SECRET_KEY is missing, Pydantic raises a validation error at startup. If APP_ENVIRONMENT=production and APP_DEBUG=true, your custom validator raises an error and prevents an insecure deployment.

Practical step-by-step: switch between local development and production-like configuration

1) Local development with a .env file

Create .env (not committed):

APP_ENVIRONMENT=development  APP_DEBUG=true  APP_DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/app_dev  APP_SECRET_KEY=dev-only-super-long-random-string-change-me  APP_CORS_ALLOW_ORIGINS=["http://localhost:5173"]

Run your app normally. The settings class loads .env and configures FastAPI accordingly.

2) Production-like run using only environment variables

Simulate production by not using .env and exporting environment variables (or setting them in your process manager/container):

export APP_ENVIRONMENT=production  export APP_DEBUG=false  export APP_DATABASE_URL='postgresql+psycopg://user:pass@db.internal:5432/app'  export APP_SECRET_KEY='a-very-long-random-production-secret-key-value'  export APP_CORS_ALLOW_ORIGINS='["https://api.example.com","https://app.example.com"]'  uvicorn app.main:app

Because the settings enforce production rules, the app will refuse to start if you forget to set CORS origins, accidentally enable debug, or omit required secrets.

3) Quick verification checklist

CheckDevelopmentProduction-like
Secrets stored.env (ignored)Environment variables / secret manager
DebugOptional trueMust be false
CORSLocal originsExplicit domains, no *
Database URLLocal DBManaged DB URL

Now answer the exercise about the content:

In a FastAPI project using a validated settings class, what is the main benefit of loading configuration from environment variables and enforcing production rules with validation?

You are right! Congratulations, now go to the next page

You missed! Try again.

Centralizing settings in environment variables and validating them helps keep environment differences explicit and lets the app fail fast if required values (like secrets) are missing or if unsafe production settings (like debug=true or wildcard CORS) are detected.

Next chapter

Testing FastAPI Applications with Pytest and TestClient

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.