Controllers, Services, Repositories, Middlewares & Request Context β Lecture Notes
1. The Request Lifecycle Inside the Server
When a client sends an HTTP request, the journey inside the server follows a well-defined path:
Client Request
β
Entry Point (OS forwards request to the server's port)
β
Middlewares (pre-routing)
β
Routing (method + path β handler mapping)
β
Middlewares (post-routing, pre-handler)
β
Controller / Handler
β
Service Layer
β
Repository Layer ββ Database
β
(response bubbles back up)
β
Middlewares (post-handler)
β
Client Response
This three-layer separation (Controller β Service β Repository) is a design pattern, not a hard requirement. A single function could do all of it β but separation makes code scalable, maintainable, and easier to debug.
2. The Three Layers
2.1 Controller / Handler
Receives: request object + response object (provided by the framework/runtime).
Responsibilities β in order:
-
Binding / Deserialization β Extract and deserialize data from the request into the languageβs native format.
- GET β extract query params and path params
- POST/PATCH/PUT β extract and deserialize request body (JSON β struct/dict/class)
- In Node.js, a body-parser middleware often handles this upstream; in Go/Python/Rust it must be done explicitly.
- If deserialization fails β return
400 Bad Request, terminate immediately.
-
Validation & Transformation β Validate the deserialized data; transform it for downstream convenience (e.g., set defaults for missing optional query params, cast types, normalize values).
- If validation fails β return
400 Bad Request, terminate immediately.
- If validation fails β return
-
Call the Service Layer β Pass validated + transformed data (plus auth context like
userId,role) to the service method. -
Send Response β Based on the service result, decide the appropriate status code and shape the response body.
- Success:
200,201,204 - Client error:
4xx - Server error:
500
- Success:
Key rule: Controllers deal with HTTP concerns only β status codes, request/response format, validation. No business logic lives here.
2.2 Service Layer
Receives: Native data from the controller (no HTTP objects β no request/response).
Responsibilities:
- Contains all business logic for the API β the actual processing.
- Orchestrates multiple repository calls, merges data, applies rules.
- Makes external calls: emails, notifications, webhooks, third-party APIs.
- Returns a result (data or error) to the controller β it never decides HTTP status codes.
Key rule: A service method should look like a plain function β you should not be able to tell from reading it that itβs part of an HTTP API. No HTTP concepts leak into this layer.
Service can:
βββ Call repo.getAllBooks(sort)
βββ Call repo.getUserById(userId)
βββ Merge results
βββ Send an email
βββ Return { books: [...] }
Service cannot:
βββ Access req / res objects
βββ Set HTTP status codes
2.3 Repository Layer
Receives: Filtered, sorted, ready-to-use data from the service layer.
Responsibilities:
- Sole responsibility: construct and execute database queries, return raw results.
- Each method does exactly one thing β fetch books, insert a book, delete by ID.
Key rule: One repository method = one data operation. Never use optional parameters to make one function do two different things.
β getBooks(sort, limit, offset) β returns all books
β getBookById(id) β returns one book
β getBooks(id?) β returns all OR one based on id
2.4 Layer Responsibilities at a Glance
| Layer | Deals With | Does NOT deal with |
|---|---|---|
| Controller | HTTP: request parsing, validation, response codes | Business logic, DB queries |
| Service | Business logic, orchestration, external calls | HTTP concepts, raw DB queries |
| Repository | DB queries: insert, fetch, update, delete | Business logic, HTTP concepts |
3. Middlewares
What is a Middleware?
A function that executes at a boundary in the request lifecycle β before routing, between routing and handler, or after handler β that can:
- Read and modify the
requestobject - Read and modify the
responseobject - Pass execution to the next step via
next() - Or short-circuit by sending a response directly (terminating the request early)
Signature: every middleware receives three things:
(request, response, next)
next()β passes execution to the next middleware or the next execution context (routing, handler, etc.)- If
next()is not called and no response is sent β request hangs.
Middlewares are optional β they may or may not be present depending on requirements.
Why Middlewares?
Same reason we use functions β avoid code duplication. Common operations that need to run for every (or many) API requests shouldnβt be copy-pasted into every handler. Delegate them to middleware.
Two requirements for something to be a middleware:
- The logic needs to run for multiple (or all) requests.
- It needs access to the
requestand/orresponseobject.
Common Middleware Examples
CORS Middleware
- Reads
Originheader from request. - If origin is in the allowed list β adds
Access-Control-Allow-Originand related headers to response. - Calls
next()to continue. - If origin not allowed β doesnβt add headers; browser will block the response.
- Why middleware: Needs to run for every request and modify response headers.
Security Headers Middleware
- Adds headers like
Content-Security-Policy,X-Frame-Options,Strict-Transport-Securityto every response. - Calls
next()to continue.
Authentication Middleware
- Extracts token (JWT or session ID) from request headers or cookies.
- Verifies the token.
- Failure β sends
401 Unauthorized, terminates request immediately (nonext()call). - Success β extracts
userId,role, permissions; stores them in request context; callsnext().
- Failure β sends
- Why middleware: Authentication must run for most protected routes without duplicating it in every handler.
Rate Limiting Middleware
- Checks how many requests this IP has made in a recent time window.
- Exceeds threshold β
429 Too Many Requests, terminate. - Under threshold β
next().
Logging / Monitoring Middleware
- Logs request method, path, query params, body, timestamp for every request.
- Always calls
next(). - Helps with debugging, auditing, and performance monitoring.
Global Error Handling Middleware
- Placed last in the middleware chain.
- Catches any unhandled error from any point in the lifecycle (handler, service, other middlewares).
- Determines whether itβs a client error (4xx) or server error (5xx).
- Sends a properly structured error response:
{ message, code, status }.
Ordering matters: Errors flow forward, not backward. If the error middleware is placed in the middle, errors from handlers further down wonβt reach it.
Typical Middleware Order
1. CORS β terminate unknown origins early
2. Security Headers β set on every response
3. Logging β record every request
4. Authentication β verify identity; set context
5. Rate Limiting β protect server resources
6. [ Routing β Handler β Service β Repository ]
7. Global Error Handler β always last
4. Request Context
What is it?
A per-request shared state β a key-value store that is:
- Scoped to a single HTTP request (not shared across requests)
- Accessible by all middlewares and handlers throughout that requestβs lifecycle
- Automatically available β every request gets its own context
Every language/framework has its own implementation, but the concept is universal.
Request A β Context A { userId: 1, role: "admin", requestId: "abc-123" }
Request B β Context B { userId: 7, role: "user", requestId: "def-456" }
These are isolated β middleware in Request A cannot read Context B.
Why it Exists
Without request context, passing data between middlewares would require directly threading values through function arguments, tightly coupling components. Context provides a decoupled shared state.
Common Uses
1. Auth data propagation
Authentication middleware verifies the token β extracts userId, role, permissions β stores in context. Downstream handlers read from context instead of trusting client-provided values.
Why not take userId from the request body?
β A malicious client could send userId of another user.
β Always use the userId from verified auth context.
2. Request ID (Distributed Tracing)
An early middleware generates a UUID and stores it as requestId in context. All subsequent logs, downstream service calls, and external API calls include this ID (e.g., in X-Request-ID header). Enables tracing a single request across multiple services in a microservice architecture.
3. Cancellation / Timeout Signals
Context can carry deadlines and abort signals. If the client disconnects or a timeout is exceeded, the signal propagates through context to downstream services, preventing them from doing unnecessary work.
Context in the Auth β Handler Flow
Auth Middleware:
verifyToken(req.headers.authorization)
β userId = 42, role = "admin"
β ctx.set("userId", 42)
β ctx.set("role", "admin")
β next()
Handler (POST /books):
userId = ctx.get("userId") // 42 β trusted, from auth
role = ctx.get("role") // "admin"
// insert book with userId = 42
// never use req.body.userId β could be spoofed
5. Full Request Lifecycle β Annotated
Client: POST /api/books { title: "SICP", authorId: 3 }
β
βΌ
ββββββββββββββββββββ
β CORS Middleware β check origin β add headers β next()
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ
β Logging Middlewareβ log: POST /api/books β next()
ββββββββββ¬ββββββββββ
βΌ
ββββββββββββββββββββ
β Auth Middleware β verify JWT β set ctx{userId,role} β next()
ββββββββββ¬ββββββββββ
βΌ
Routing
βΌ
ββββββββββββββββββββββββββββββββββββββββββ
β Controller (POST /api/books handler) β
β 1. Bind: extract req.body β
β 2. Validate: title is string, etc. β
β 3. Transform: set defaults β
β 4. Call service.createBook(data, ctx) β
ββββββββββββββββββ¬ββββββββββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββ
β Service (createBook) β
β - business logic β
β - call repo.insertBook(data) β
β - send confirmation email β
β - return { book } β
ββββββββββββββββββ¬ββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββ
β Repository (insertBook) β
β - construct INSERT query β
β - execute β
β - return created row β
ββββββββββββββββββ¬ββββββββββββ
β result flows back up
βΌ
Controller: return 201 Created + { book }
βΌ
ββββββββββββββββββββββββββββ
β Global Error Handler β (only fires if error was thrown)
ββββββββββββββββββββββββββββ
βΌ
Client receives response
Quick Revision Checklist
- Controller responsibilities: bind request data β validate/transform β call service β send response
- Controller is the only layer that deals with HTTP (status codes, req/res objects)
- Service layer: business logic, orchestration, external calls β no HTTP concepts
- Repository layer: one method = one DB operation; no business logic
- Middleware signature:
(request, response, next)βnext()passes to the next context - Middleware short-circuits by sending a response and NOT calling
next() - Common middlewares: CORS, security headers, authentication, rate limiting, logging, global error handler
- Global error handler goes last β ordering of middlewares is critical
- Request context: per-request key-value store, accessible to all middlewares and handlers
- Always read
userIdfrom auth context, never from the request body β prevents spoofing - Context use cases: auth data, request ID for distributed tracing, cancellation/timeout signals