Web Service Architecture
- Restructure a FastAPI app into routes, services, and repositories with clean boundaries
- Explain dependency injection and use FastAPI Depends for auth and DB access
- Add middleware for request IDs and timing, and consistent problem-detail errors
- Implement simple rate limiting and explain where it belongs in the stack
- Apply 12-factor config so the same code runs in dev and prod
| Spaced-rep warm-up: due flashcards (auth + networking) | 10 min |
| ELI5 + tech read: back-of-house layering | 15 min |
| Guided: carve the monolith + middleware | 40 min |
| Practice: token-bucket rate limiter | 20 min |
| Project: harden the Day 42 service | 25 min |
| Quiz + flashcards | 10 min |
Builds on: Day 42 β The API + DB mini-service Β· Day 44 β AuthN & AuthZ Β· Day 16 β Logging, config & CLI ergonomics
Walk into a good restaurant's back of house and you will not find one heroic cook doing everything. The waiter (routes) takes orders and speaks "menu" to customers β they never touch a pan. The kitchen (service layer) knows the recipes β the business logic β but never talks to diners. The pantry crew (repository layer) fetches ingredients from storage and is the only team that knows which shelf holds what. Between the door and the waiter stand the host and bouncer (middleware): greeting everyone the same way, checking reservations, throwing out troublemakers β before any order is taken.
When a restaurant mixes these jobs β the waiter cooking, the chef running to the cellar β small changes cause chaos: change the menu wording and somehow the soup breaks. Layered services exist for the same reason: each layer speaks only to its neighbors, so you can swap the pantry (SQLite β Postgres), rewrite a recipe (business rule), or retrain the waiters (API version) without the other layers noticing. Today you rebuild your Day 42 service this way and give it a bouncer.
Every LLM backend you will build β the capstone included β is a FastAPI-shaped service: routes receiving chat requests, a service layer orchestrating retrieval and model calls, repositories wrapping the vector store and DB. Teams reject FDE prototype code that tangles these layers because they cannot extend it. Middleware is where production non-negotiables live: request IDs that let you find one user's failing request in a million log lines (the tracing habit Day 142 formalizes), rate limits that stop one customer's retry storm from bankrupting your token budget. This architecture is the difference between a demo and something a customer can run.
Guided practice
Carve the monolith into layers
25 minTake your Day 42 notes service (or the reference shape in the starter) and restructure it.
- Create the package layout:
app/routes/notes.py,app/services/notes.py,app/repos/notes.py,app/deps.py,app/main.py. - Move ALL SQL into the repo class. Its methods speak domain language (
list_for_owner,add,delete), not SQL. - Move rules into the service: owner checks, title validation. The service raises domain exceptions (
NotFound,Forbidden) β it has no idea HTTP exists. - Routes become thin: parse β call service β return. Wire the repo and current user via
Depends. - Prove the payoff: write one service test using an in-memory fake repo β no HTTP, no database, runs in milliseconds.
Middleware: request IDs, timing, honest errors
15 min- Add the middleware from the starter to your app: every request gets a short request ID, is timed, and logged as one structured line.
- Add the exception handlers: domain
NotFoundβ 404 problem-details,Forbiddenβ 403,ValueErrorβ 422. Confirm all errors now share one JSON shape. - Hit a route with the docs UI, find your request ID in the logs, and confirm the same ID came back in the
X-Request-IDresponse header β this is how you will correlate a user's bug report to a log line. - Trigger each error deliberately and screenshot-or-paste the consistent bodies into your notes.
On your own
Build a token-bucket rate limiter
20 minImplement rate limiting for your service with no library: a token bucket per API key (capacity 10, refill 1 token/second), enforced in middleware or a dependency, returning 429 with a Retry-After header when empty.
Constraints: pure stdlib; buckets in a module-level dict (note in a comment why this breaks with multiple instances β Day 46 explains, Day 47 fixes it with Redis); a unit test that fires 11 instant requests and asserts the 11th gets 429; a second test that sleeps past a refill and asserts recovery.
Hints (only if stuck): store (tokens, last_refill_time) per key; on each request, first credit elapsed_time Γ rate up to capacity, then try to spend 1.
Harden the Day 42 service
Ship v1 of the notes service with today's architecture: layered packages (routes/services/repos), API-key auth using Day 44's auth_lib wired through Depends (keys stored hashed; a POST /v1/keys bootstrap route returns a key once), request-ID + timing middleware, problem-details errors, your token-bucket limiter, /v1 path versioning, and all config (DB path, rate limit, admin bootstrap secret) from env vars with sane defaults. Tests: service-layer tests on a fake repo plus TestClient tests proving 401 without a key, 429 over the limit, and consistent error bodies. This service is your template for every API in the program β the capstone (Day 119) starts from this shape.
Common mistakes & misconceptions
- Putting business logic in routes "because it is short." It never stays short, and logic in routes can only be tested through HTTP. Routes translate; services decide.
- Letting SQL leak into services. The repo boundary is what makes storage swappable and service tests fast; one stray query breaks both.
- Skipping dependency injection and importing globals directly. Hard-wired dependencies make tests require real databases and real keys; Depends + overrides keep tests honest and fast.
- Inventing a new error JSON per endpoint. Clients end up parsing four shapes; one problem-details handler ends it.
- Rate limiting inside handlers after work is done. Limit at the edge (middleware/dependency) BEFORE expensive work β the point is protecting the kitchen, not scolding afterwards.
- Deferring versioning "until v2." Once clients exist, unversioned paths are permanent contracts. /v1 costs nothing today and saves a migration later.
Q1. In a layered service, which statement about dependencies is correct?
Q2. What is the main testing benefit of injecting the repository into the service rather than constructing it inside?
Q3. A token bucket has capacity 10 and refills 1 token/sec. A client idle for an hour then bursts 15 instant requests. What happens?
Go deeper β curated resources
- RFC 9457 β Problem Details for HTTP APIs β The standard behind the error shape you built: media type application/problem+json and the meaning of each field.
- Layered restructure done; service test passes with fake repo
- Request IDs visible in logs and response headers
- Rate limiter passes both unit tests (11th request 429; refill recovers)
- Hardened service committed: auth, versioning, env config, tests green
- Quiz β₯ 2/3
β Back: This is Day 42's service grown up: Day 44's auth_lib guards the door, Day 16's logging and env-config habits fill the middleware and settings, and the repo layer isolates the SQL you learned on Days 36β38.
Forward β: Day 46 explains why your limiter's in-memory dict breaks the moment you run two instances β statelessness β and Day 47 moves that state to a shared cache. The capstone service (Day 119 onward) starts from today's template, and Day 142's tracing upgrades your request IDs into full spans.
Unlocks: D46 Distributed Systems Fundamentals Β· D47 Caching & Queues Β· D49 Week 7 Checkpoint: Design & Build a URL Shortener Β· D169 Enterprise Integration