FastAPI’yi anlatmanın en iyi yolu küçük ama gerçek bir uygulama geliştirmek. ÖzgürKon 2026 atölyesi için hazırladığım Expense Tracker API, kullanıcıların hesap oluşturabildiği, giriş yapabildiği ve kendi harcamalarını yönetebildiği bir demo proje.
Bu yazıda tek tek endpoint yazmaktan çok, uygulamanın parçalarını nasıl ayırdığımı ve bir request’in sistem içinde hangi layer’lardan geçtiğini anlatacağım.
Projenin kaynak koduna GitHub üzerinden, çalışan sürümüne ise FastAPI Cloud üzerinden ulaşabilirsiniz.
Uygulama ne yapıyor?
Proje iki temel alan üzerine kurulu:
- JWT tabanlı register, login, token refresh ve logout flow’u
- Harcama ekleme, listeleme, güncelleme, silme ve aylık özet oluşturma
Harcama kayıtları kullanıcıya ait. Bir kullanıcı yalnızca kendi verilerini görebiliyor ve değiştirebiliyor. Listeleme endpoint’i ay, kategori, para birimi ve tutar aralığına göre filtrelemeyi; sayfalama yapmayı da destekliyor.
Tech stack
Projenin dependency’leri pyproject.toml dosyasında tutuluyor:
- FastAPI: HTTP layer, dependency injection ve OpenAPI dokümantasyonu
- Pydantic ve Pydantic Settings: Request validation, response schema ve configuration
- SQLAlchemy 2.0: Async database access ve ORM layer
- PostgreSQL ve psycopg: Data persistence
- Alembic: Database migration’ları ve seed data
- PyJWT: Access token üretme ve doğrulama
- uv: Dependency ve virtual environment yönetimi
- Ruff: Code style ve static analysis
Project structure
Uygulamayı sorumluluklarına göre küçük layer’lara ayırdım:
ozgurkon/├── app/│ ├── main.py│ ├── api/│ │ ├── deps.py│ │ └── v1/routes/│ │ ├── auth.py│ │ ├── expenses.py│ │ └── users.py│ ├── core/│ │ ├── config.py│ │ ├── db.py│ │ ├── exceptions.py│ │ ├── middleware.py│ │ ├── responses.py│ │ └── security.py│ ├── crud/│ ├── models/│ ├── schemas/│ └── services/├── alembic/├── alembic.ini└── pyproject.tomlBu yapıdaki temel akış şöyle:
HTTP request → route → Pydantic schema → service → CRUD → SQLAlchemy model → PostgreSQLHer layer’ın sınırı belli olduğunda endpoint’ler kısa kalıyor; validation, business rule ve database query’leri birbirine karışmıyor.
Application entry point
app/main.py, uygulamanın bootstrap edildiği yer. Router’lar, middleware ve exception handler’lar burada bir araya geliyor:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanagerasync def lifespan(app: FastAPI): if settings.create_tables_on_startup: await create_db_and_tables() yield
def create_app() -> FastAPI: app = FastAPI( title=settings.project_name, version=settings.version, lifespan=lifespan, )
app.add_middleware(RequestResponseLoggingMiddleware) setup_exception_handlers(app) app.include_router(api_v1_router, prefix=settings.api_v1_prefix) return app
app = create_app()Application factory pattern’i, kurulum adımlarını tek yerde topluyor. lifespan ise startup ve shutdown sırasında çalışması gereken işleri yönetiyor.
Configuration’ı koddan ayırmak
Database connection, JWT settings ve token süreleri BaseSettings üzerinden environment variable’lardan okunuyor:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): project_name: str = "Expense Tracker API" api_v1_prefix: str = "/api/v1" database_con: str secret_key: str access_token_expire_minutes: int = 30 refresh_token_expire_days: int = 7
model_config = SettingsConfigDict(env_file=".env")Bu sayede development ve production ortamları aynı kodu, farklı configuration değerleriyle çalıştırabiliyor. Gerçek bir production ortamında secret_key ve database bilgileri kesinlikle source code içinde tutulmamalı.
Pydantic ile request validation
Bir harcama kaydının tutarı pozitif olmalı, para birimi ve kategorisi tanımlı değerlerden gelmeli, harcama tarihi gelecekte olmamalı:
class ExpenseBase(BaseModel): amount: Decimal = Field(gt=0, max_digits=12, decimal_places=2) currency: CurrencyEnum category: ExpenseCategoryEnum description: str | None = Field(default=None, max_length=255) spent_at: datetime
@field_validator("spent_at") @classmethod def spent_at_cannot_be_future(cls, value: datetime) -> datetime: now = datetime.now(value.tzinfo) if value.tzinfo else datetime.now() if value > now: raise ValueError("spent_at cannot be in the future") return valueKurallar schema içinde tanımlandığı için route’a ulaşan veri artık beklediğimiz biçimde oluyor. Aynı schema OpenAPI dokümantasyonuna da yansıyor.
Dependency injection ile user ve database session
FastAPI’nin dependency injection yapısını database session ve current user için kullanıyorum:
DbSession = Annotated[AsyncSession, Depends(get_db)]CurrentUser = Annotated[User, Depends(get_current_user)]
@router.post("", status_code=status.HTTP_201_CREATED)async def create_expense( payload: ExpenseCreate, db: DbSession, current_user: CurrentUser,): return await expense_service.create_expense( db=db, user_id=current_user.id, payload=payload, )Endpoint’in authentication veya connection lifecycle ayrıntılarını bilmesine gerek kalmıyor. İhtiyacı olan dependency’leri type’larıyla birlikte bildiriyor.
Service ve CRUD layer’ları
Route yalnızca HTTP ile ilgileniyor. Harcamanın gerçekten var olup olmadığı gibi business rule’lar service layer’da, SQL query’leri ise CRUD layer’da tutuluyor:
async def get_expense(db, user_id, expense_id): expense = await expense_crud.get_expense_by_id( db=db, user_id=user_id, expense_id=expense_id, ) if expense is None: raise ExpenseNotFoundException()
return ExpenseResponse.model_validate(expense)Query’ye user_id eklenmesi önemli. Kaydın ID’si bilinse bile başka bir kullanıcıya ait harcamaya erişilmesini engelliyor.
JWT ve refresh token akışı
Login başarılı olduğunda kısa ömürlü bir access token ve daha uzun ömürlü bir refresh token üretiliyor. Access token JWT olarak imzalanıyor; refresh token ise plain text olarak değil, SHA-256 hash’iyle database’de saklanıyor.
Token refresh sırasında eski refresh token revoke edilip yeni bir token pair oluşturuluyor. Logout işlemi de refresh token’ı geçersiz kılıyor. Böylece access token’ları kısa tutarken kullanıcıyı her yarım saatte bir yeniden login olmaya zorlamıyoruz.
Projeyi çalıştırmak
Önce örnek environment file’ını kopyalayıp dependency’leri kurun:
cp .env.example .envuv syncArdından development server’ı başlatın:
fastapi dev app/main.pyMigration’ları ve default user’ı oluşturmak için:
uv run alembic upgrade headUygulama çalıştığında Swagger UI’a /docs adresinden ulaşabilir, register ve login flow’unu doğrudan tarayıcıdan deneyebilirsiniz.
Sonuç
Bu proje, FastAPI ile birkaç endpoint yazmanın ötesine geçip büyümeye açık bir API’nin temel parçalarını gösteriyor: net layer boundary’leri, validated request data, async database access, centralized exception handling ve refresh token flow’u.
Atölyedeki asıl fikir framework’ün bütün özelliklerini ezberlemek değil; bir HTTP request’in uygulama içinde nasıl ilerlediğini ve her sorumluluğun nerede yaşaması gerektiğini görmekti.

Yorumlar
Yorumlar yükleniyor…