Merapikan konfigurasi aplikasi dengan pydantic-settings: memindahkan secret ke variabel environment, file .env, kelas Settings bertipe dengan validasi otomatis, hingga pola environment-specific untuk development dan produksi.

Sejauh ini kita sudah beberapa kali menulis nilai sensitif langsung di kode — SECRET_KEY = "ganti-dengan-secret-kuat" di episode 12, dan DATABASE_URL di episode 9. Di dunia nyata, ini kesalahan fatal: secret di kode = secret yang bocor saat repo di-commit. Episode ini memperbaiki itu semua dengan konfigurasi terpusat.
Mengapa episode ini penting? Aplikasi produksi butuh konfigurasi yang berbeda-beda per environment — database staging vs produksi, secret yang berbeda, fitur yang diaktifkan. Menulis semuanya sebagai konstanta di kode membuat aplikasi tidak bisa dideploy dengan aman. Jawabannya adalah 12-factor app: konfigurasi hidup di environment.
pip install pydantic-settingspydantic-settings memperluas Pydantic untuk membaca konfigurasi dari environment variable dan file .env — dengan validasi dan tipe otomatis.
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
app_name: str = "FastAPI Lab"
debug: bool = False
database_url: str = "sqlite:///./app.db"
secret_key: str
access_token_expire_minutes: int = 30
settings = Settings()Setiap field kelas adalah setting. Aturan kerjanya:
secret_key: str tanpa default → wajib ada di .env atau environment. Jika tidak ada, app gagal start — lebih baik dari pada start dengan secret kosong.database_url: str = "sqlite:///..." → punya default development, bisa di-override di produksi.debug: bool membaca "true"/"false" dari string environment jadi boolean.Sekarang nilai-nilai kita pindah dari kode ke .env:
APP_NAME=FastAPI Lab
DEBUG=false
DATABASE_URL=postgresql+psycopg://user:password@localhost/appdb
SECRET_KEY=super-rahasia-jangan-di-commit
ACCESS_TOKEN_EXPIRE_MINUTES=30Penting: .env berisi secret, jadi jangan pernah di-commit ke git. Tambahkan ke .gitignore:
.env
.venv/
__pycache__/
uploads/Untuk mendokumentasikan variabel yang dibutuhkan tanpa membocorkan nilainya, buat .env.example yang boleh di-commit — ini pola standar tim:
DATABASE_URL=
SECRET_KEY=Important
Prioritas baca pydantic-settings: environment variable > file .env > default di kode. Ini artinya konfigurasi yang di-set di environment (misal di Docker/Kubernetes, episode 24) otomatis menang atas .env. Pola inilah yang membuat satu kode bisa jalan di banyak environment.
Butuh setting berbeda untuk development dan produksi? Tiga pendekatan yang saling melengkapi:
DATABASE_URL dan SECRET_KEY sebagai env, tanpa mengubah kode.SettingsConfigDict(env_file=f".env.{app_env}") untuk .env.dev, .env.prod.ENV=development|production di dalam Settings.Pendekatan 1 adalah yang paling umum dan paling sederhana: kode tetap satu, environment menentukan segalanya. Kita pakai pendekatan ini di episode 24.
Karena Settings() dipanggil di import, dan konfigurasi tidak berubah saat runtime, instance settings bisa di-share. Di FastAPI, dependency adalah cara idiomatis menyebarkannya:
from functools import lru_cache
from typing import Annotated
from fastapi import Depends
from app.config import Settings
@lru_cache
def get_settings() -> Settings:
return Settings()
SettingsDep = Annotated[Settings, Depends(get_settings)]
@app.get("/info/")
def app_info(settings: SettingsDep) -> dict[str, str]:
return {"app_name": settings.app_name, "debug": settings.debug}@lru_cache memastikan Settings hanya dibuat sekali (hasil di-cache) — dependency ini dipanggil di banyak endpoint tanpa biaya berulang.
Tip
Jangan membuat instance Settings() baru di dalam setiap handler — selain boros, ia juga membuat kode sulit di-test (karena membaca .env sungguhan). Dependency get_settings memberi kalian titik override yang bersih di test — persis pola dependency override di episode 10.
Ingat SECRET_KEY dan ALGORITHM di episode 12? Sekarang dipindah ke Settings:
import jwt
from app.config import settings
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.access_token_expire_minutes
)
payload = {"sub": subject, "exp": expire}
return jwt.encode(
payload, settings.secret_key, algorithm="HS256"
)Mengganti konstanta dengan settings.xxx membuat seluruh konfigurasi hidup di satu tempat — dan semua perubahan hanya menyentuh .env.
| Pitfall | Solusi |
|---|---|
.env ter-commit ke git | Tambahkan ke .gitignore; hanya .env.example yang di-commit |
| Secret tidak wajib | Setting tanpa default gagal start jika kosong |
| Konfigurasi hardcoded di kode | Pindah semua ke Settings |
File .env salah path | Cek env_file relatif terhadap working directory |
| Setting baru tidak terbaca | Pastikan ada di .env dan nama field cocok |
Inti yang harus dibawa pulang:
.env, dan default — dengan tipe & validasi.SECRET_KEY, DATABASE_URL, dan kawan-kawan pindah dari kode ke environment..gitignore untuk .env; .env.example sebagai dokumentasi.@lru_cache → satu instance, mudah di-test.Di episode 17 selanjutnya kita akan membahas logging & error handling — HTTPException yang terstruktur, custom exception handler untuk format error konsisten, serta structured logging yang siap untuk observability. Di sinilah API kalian mulai "dewasa" dan bisa didiagnosis saat produksi bermasalah!