Architecture Overview
CMS API follows a layered architecture where each layer has a single responsibility and communicates only with the layer directly below it.
Layers
┌─────────────────────────────────────┐
│ HTTP Request │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Router │ app/features/{domain}/{module}/router.py
│ Receives request, returns response │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Schema │ app/features/{domain}/{module}/schema.py
│ Validates input, shapes output │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Service │ app/features/{domain}/{module}/service.py
│ Business logic and decisions │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Repository │ app/features/{domain}/{module}/repository.py
│ Database communication only │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Model │ app/features/{domain}/{module}/model.py
│ SQLAlchemy table definitions │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ PostgreSQL │
└─────────────────────────────────────┘
Layer Responsibilities
Router
- Receives HTTP requests
- Invokes the appropriate service method
- Returns HTTP responses with correct status codes
- Has no business logic
Schema (Pydantic)
- Validates and deserializes incoming request data
- Serializes outgoing response data
- Ensures sensitive fields like
password_hashnever leak in responses
Service
- Contains all business logic
- Decides what is allowed, what happens, and in what order
- Coordinates multiple repository calls within a single transaction
- Raises appropriate exceptions on business rule violations
- Modules communicate only through service interfaces — never directly through repositories or models of another module
Repository
- The only layer that communicates with the database
- Executes SQLAlchemy queries
- Uses
flush()instead ofcommit()— transaction control belongs to the service layer - Never contains business logic
Model
- Defines database tables as Python classes
- Declares relationships between tables
- Contains only simple helper properties
- Pivot tables (many-to-many) are defined in the model file of the owning module
Request Flow Example
POST /api/v1/auth/login
1. Router receives { email, password }
2. LoginRequest validates schema — correct format?
3. AuthService is email registered? is password correct? is user active?
4. UserRepository SELECT user WHERE email = ?
5. AuthService hash comparison, generate tokens
6. TokenRepository INSERT refresh token
7. UserRepository UPDATE last_login
8. LoginResponse serialize user + tokens
9. Router return 200 OK
Transaction Management
Repositories use flush() to stage changes without committing. The get_db() dependency in database.py owns the transaction lifecycle:
Request starts → session opens
All operations → flush() (staged, not committed)
Request succeeds → commit()
Exception raised → rollback()
Request ends → session closes
This ensures that multiple repository calls within a single service method are always atomic.
Module Boundaries
Modules are organized into feature domains. Cross-domain communication happens only at the service level:
✅ cms.posts.service → auth.users.service (via dependency injection)
✅ cms.posts.service → cms.media.repository (within same domain is acceptable)
❌ cms.posts.repository → auth.users.model (never cross model boundaries)
This separation makes it possible to extract a domain into a separate service in the future with minimal refactoring.