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/api/v1/
│ Receives request, returns response │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Schema │ app/schemas/
│ Validates input, shapes output │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Service │ app/services/
│ Business logic and decisions │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Repository │ app/repositories/
│ Database communication only │
└──────────────────┬──────────────────┘
│
┌──────────────────▼──────────────────┐
│ Model │ app/models/
│ 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
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
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.