MVC Framework
A web framework from scratch. Front controller dispatches requests to route-matched handlers, middleware processes the pipeline, and adapters normalize different handler signatures. How Flask and Spring MVC work under the hood.
Key Abstractions
Single entry point for all incoming requests. Receives the raw request, runs middleware, delegates to the router, and returns the final response.
Maps URL patterns to handler functions. Supports path parameters like /users/:id by converting route patterns into regex and extracting named groups on match.
Normalizes different handler signatures to a common interface. A handler taking (request) and one taking (request, path_params) both work without the caller knowing the difference.
Chain of responsibility for cross-cutting concerns like auth, logging, and CORS. Each middleware runs before and after the handler, with the option to short-circuit.
Data classes carrying method, path, headers, query parameters, and body through the framework. Response holds status code, body, and headers.
Template Method for rendering output. Subclasses decide the format: JSON serialization, HTML templating, or plain text. The handler returns data, the resolver turns it into a response body.
Class Diagram
How It Works
Every HTTP request enters through one object: the front controller. Not a different handler per URL. Not a servlet per endpoint. One entry point that coordinates everything.
Here is the lifecycle. A request arrives at the front controller. It passes through the middleware chain first. Logging middleware records the start time. Auth middleware checks the token and either lets the request continue or short-circuits with a 401. If the chain completes, the front controller hands the request to the router.
The router holds a list of route patterns. Each pattern like /users/:id gets compiled into a regex on registration. When a request for /users/42 comes in, the router walks its routes, tries each regex, and pulls out id=42 as a path parameter. First match wins.
Once the router finds a match, the handler adapter figures out how to call the handler. Your health check function takes zero arguments. Your user listing takes a Request. Your user detail handler takes Request plus path_params. The adapter inspects the function signature and calls it with the right arguments. You never write boilerplate to conform every handler to one rigid interface.
The handler returns raw data, not a formatted string. A dict like {"users": ["alice", "bob"]}. The view resolver picks up from there. JsonViewResolver turns it into JSON. PlainTextViewResolver produces key-value lines. Swap the resolver and every handler's output format changes. No handler code touched.
Requirements
Functional
- Single entry point (front controller) that receives all requests and coordinates routing, middleware, and rendering
- Route registration with URL pattern matching and path parameter extraction (/users/:id)
- Handler adapter that normalizes different handler signatures (zero-arg, one-arg, two-arg)
- Middleware chain with short-circuit capability (auth rejects before reaching the handler)
- Pluggable view resolvers for JSON, plain text, or other formats
Non-Functional
- Routes compiled to regex once at registration, not on every request
- Middleware order is explicit and deterministic
- Adding a new route is a one-line decorator call, not a class hierarchy
- View resolver is swappable without changing any handler code
Design Decisions
Why a Front Controller instead of one handler per URL?
One handler per URL works when you have three endpoints. But cross-cutting concerns like auth, logging, and error handling get duplicated in every handler. A front controller centralizes all of that. Middleware runs once in one place. Routing lives in one table. You can see your entire URL space by looking at one object. Spring MVC's DispatcherServlet and Django's URL dispatcher both follow this pattern for exactly these reasons.
Why use an Adapter for handler normalization?
Without an adapter, every handler must match a single signature like handle(Request) -> Response. Your health check that returns {"status": "ok"} with no request data still needs to accept a Request parameter it ignores. The adapter inspects the handler's signature and calls it appropriately. Zero args? Fine. Two args with path params? Also fine. This is how Flask works: your route handler can take keyword arguments matching path parameters, or take nothing at all.
Why middleware as a chain instead of per-handler decorators?
Per-handler decorators (@auth @log def my_handler) mean you decorate every single handler. Miss one and you have an unprotected endpoint. A middleware chain applies to all requests by default. You opt out of specific middleware for specific routes, not opt in. The chain also gives you a clear before-and-after lifecycle. Logging middleware starts a timer before the handler and reads it after. A decorator can do this too, but the chain makes ordering between multiple concerns explicit.
Why separate the ViewResolver from the handler?
Handlers should return data, not formatted strings. If your handler builds a JSON string manually, what happens when a client wants XML? You rewrite the handler. With a view resolver, you swap JsonViewResolver for XmlViewResolver and every handler works with the new format. Content negotiation becomes a resolver selection problem, not a handler rewrite.
Interview Follow-ups
- "How would you add async request handling?" Replace the synchronous
handle(Request) -> Responsewithasync handle(Request) -> Responseor aCompletableFuture<Response>in Java. The front controller runs the middleware chain on an event loop. Each middleware awaits the next handler. The structure stays identical; the execution model shifts from blocking threads to non-blocking callbacks or coroutines. This is the jump from Flask to FastAPI or from Spring MVC to Spring WebFlux. - "How would you support route groups or namespaces?" Add a
group(prefix)method to the App that returns a sub-router. Routes registered on the sub-router get the prefix prepended.app.group("/api/v1")means.route("/users")becomes/api/v1/users. You can also attach middleware to a group so only those routes get auth, for example. Express.js Router and Django'sinclude()both work this way. - "How would you handle content negotiation?" Check the
Acceptheader on the incoming request. If the client sendsAccept: application/xml, the front controller picksXmlViewResolver. IfAccept: text/html, it picks the HTML resolver. The Strategy pattern: the same handler, different rendering strategy chosen at runtime based on the request. - "How would you add error-handling middleware?" Insert a middleware early in the chain that wraps the
next.handle()call in a try/catch. If any downstream middleware or handler throws, the error handler catches it, logs the stack trace, and returns a 500 response with a safe error message. You can also map specific exception types to specific status codes:NotFoundExceptionto 404,ValidationErrorto 400. Express.js does this with four-argument middleware(err, req, res, next).
Code Implementation
1 from __future__ import annotations
2 from dataclasses import dataclass, field
3 from abc import ABC, abstractmethod
4 from typing import Any, Callable, Optional
5 import re
6 import json
7 import time
8
9
10 # ─── Data Classes ───────────────────────────────────────────────
11
12 @dataclass
13 class Request:
14 method: str
15 path: str
16 headers: dict = field(default_factory=dict)
17 body: str = ""
18 path_params: dict = field(default_factory=dict)
19
20
21 @dataclass
22 class Response:
23 status_code: int
24 body: str
25 headers: dict = field(default_factory=dict)
26
27
28 # ─── Handler Type ──────────────────────────────────────────────
29
30 Handler = Callable[[Request], Response]
31
32
33 # ─── View Resolver (Template Method) ───────────────────────────
34
35 class ViewResolver(ABC):
36 """Template Method: subclasses decide how to serialize data."""
37
38 def render(self, data: Any) -> Response:
39 body = self.serialize(data)
40 content_type = self.content_type()
41 return Response(200, body, {"Content-Type": content_type})
42
43 @abstractmethod
44 def serialize(self, data: Any) -> str:
45 ...
46
47 @abstractmethod
48 def content_type(self) -> str:
49 ...
50
51
52 class JsonViewResolver(ViewResolver):
53 def serialize(self, data: Any) -> str:
54 return json.dumps(data, indent=2)
55
56 def content_type(self) -> str:
57 return "application/json"
58
59
60 class PlainTextViewResolver(ViewResolver):
61 def serialize(self, data: Any) -> str:
62 if isinstance(data, dict):
63 return "\n".join(f"{k}: {v}" for k, v in data.items())
64 return str(data)
65
66 def content_type(self) -> str:
67 return "text/plain"
68
69
70 # ─── Route and Router ─────────────────────────────────────────
71
72 @dataclass
73 class Route:
74 method: str
75 pattern: str
76 handler: Any # raw handler, any signature
77 regex: re.Pattern = field(init=False)
78 param_names: list[str] = field(init=False)
79
80 def __post_init__(self):
81 # Convert /users/:id into a regex with named groups
82 self.param_names = re.findall(r":(\w+)", self.pattern)
83 regex_pattern = self.pattern
84 for name in self.param_names:
85 regex_pattern = regex_pattern.replace(f":{name}", f"(?P<{name}>[^/]+)")
86 self.regex = re.compile(f"^{regex_pattern}$")
87
88
89 @dataclass
90 class RouteMatch:
91 handler: Any
92 params: dict
93
94
95 class Router:
96 """Maps URL patterns to handlers. Supports path parameters."""
97
98 def __init__(self):
99 self._routes: list[Route] = []
100
101 def add_route(self, method: str, pattern: str, handler: Any) -> None:
102 self._routes.append(Route(method.upper(), pattern, handler))
103
104 def match(self, method: str, path: str) -> Optional[RouteMatch]:
105 for route in self._routes:
106 if route.method != method.upper():
107 continue
108 m = route.regex.match(path)
109 if m:
110 return RouteMatch(handler=route.handler, params=m.groupdict())
111 return None
112
113
114 # ─── Handler Adapter (Adapter Pattern) ─────────────────────────
115
116 class HandlerAdapter:
117 """
118 Normalizes different handler signatures into a unified call.
119
120 Some handlers take just (request). Others take (request, path_params).
121 A health check might take nothing at all. The adapter figures out
122 what the handler expects and calls it correctly.
123 """
124
125 @staticmethod
126 def invoke(handler: Any, request: Request) -> Any:
127 import inspect
128 sig = inspect.signature(handler)
129 param_count = len(sig.parameters)
130
131 if param_count == 0:
132 return handler()
133 elif param_count == 1:
134 return handler(request)
135 else:
136 return handler(request, request.path_params)
137
138
139 # ─── Middleware (Chain of Responsibility) ───────────────────────
140
141 class Middleware(ABC):
142 """Each middleware processes the request and decides whether to continue."""
143
144 @abstractmethod
145 def process(self, request: Request, next_handler: Handler) -> Response:
146 ...
147
148
149 class AuthMiddleware(Middleware):
150 def __init__(self, valid_tokens: set[str]):
151 self.valid_tokens = valid_tokens
152
153 def process(self, request: Request, next_handler: Handler) -> Response:
154 auth = request.headers.get("Authorization", "")
155 token = auth.replace("Bearer ", "") if auth.startswith("Bearer ") else ""
156 if token not in self.valid_tokens:
157 print(f" [AUTH] REJECTED: invalid token '{token}'")
158 return Response(401, "Unauthorized")
159 print(f" [AUTH] OK: token '{token}'")
160 return next_handler(request)
161
162
163 class LoggingMiddleware(Middleware):
164 def process(self, request: Request, next_handler: Handler) -> Response:
165 print(f" [LOG] >>> {request.method} {request.path}")
166 start = time.time()
167 response = next_handler(request)
168 elapsed_ms = (time.time() - start) * 1000
169 print(f" [LOG] <<< {response.status_code} ({elapsed_ms:.1f}ms)")
170 return response
171
172
173 # ─── App: Front Controller + Route Decorator ───────────────────
174
175 class App:
176 """
177 Mini Flask. The App is the front controller: single entry point
178 that coordinates routing, middleware, handler adaptation, and
179 view resolution.
180 """
181
182 def __init__(self, view_resolver: ViewResolver | None = None):
183 self._router = Router()
184 self._middlewares: list[Middleware] = []
185 self._adapter = HandlerAdapter()
186 self._view_resolver = view_resolver or JsonViewResolver()
187
188 def route(self, pattern: str, method: str = "GET"):
189 """Decorator to register a handler for a URL pattern."""
190 def decorator(fn):
191 self._router.add_route(method, pattern, fn)
192 return fn
193 return decorator
194
195 def use(self, middleware: Middleware) -> App:
196 self._middlewares.append(middleware)
197 return self
198
199 def handle_request(self, request: Request) -> Response:
200 """Front Controller: the single entry point."""
201
202 # Build the innermost handler: routing + adaptation + view resolution
203 def core_handler(req: Request) -> Response:
204 match = self._router.match(req.method, req.path)
205 if match is None:
206 return Response(404, "Not Found")
207
208 req.path_params = match.params
209 raw_result = self._adapter.invoke(match.handler, req)
210
211 # If handler returned a Response directly, use it as-is
212 if isinstance(raw_result, Response):
213 return raw_result
214
215 # Otherwise, run through the view resolver
216 return self._view_resolver.render(raw_result)
217
218 # Wrap core_handler with middleware chain (innermost last)
219 handler = core_handler
220 for mw in reversed(self._middlewares):
221 prev = handler
222 handler = lambda req, _mw=mw, _prev=prev: _mw.process(req, _prev)
223
224 return handler(request)
225
226
227 # ─── Demo ──────────────────────────────────────────────────────
228
229 if __name__ == "__main__":
230 app = App(view_resolver=JsonViewResolver())
231
232 # Register middleware
233 app.use(LoggingMiddleware())
234 app.use(AuthMiddleware(valid_tokens={"secret-123", "admin-456"}))
235
236 # Register routes using the decorator
237 @app.route("/users", method="GET")
238 def list_users(request: Request):
239 return {"users": ["alice", "bob", "charlie"], "count": 3}
240
241 @app.route("/users/:id", method="GET")
242 def get_user(request: Request, path_params: dict):
243 user_id = path_params["id"]
244 return {"id": user_id, "name": f"User {user_id}", "active": True}
245
246 @app.route("/health", method="GET")
247 def health_check():
248 return {"status": "ok"}
249
250 # ── Test 1: List users (valid token) ────────────────────────
251 print("=" * 60)
252 print("TEST 1: GET /users with valid token")
253 print("=" * 60)
254 resp = app.handle_request(Request(
255 method="GET", path="/users",
256 headers={"Authorization": "Bearer secret-123"},
257 ))
258 print(f" Status : {resp.status_code}")
259 print(f" Body : {resp.body}")
260 print(f" Headers: {resp.headers}\n")
261
262 # ── Test 2: Get user by ID (path parameter extraction) ─────
263 print("=" * 60)
264 print("TEST 2: GET /users/42 with path parameter")
265 print("=" * 60)
266 resp = app.handle_request(Request(
267 method="GET", path="/users/42",
268 headers={"Authorization": "Bearer admin-456"},
269 ))
270 print(f" Status : {resp.status_code}")
271 print(f" Body : {resp.body}")
272 print(f" Headers: {resp.headers}\n")
273
274 # ── Test 3: Auth rejection (bad token) ─────────────────────
275 print("=" * 60)
276 print("TEST 3: GET /users with bad token (auth rejects)")
277 print("=" * 60)
278 resp = app.handle_request(Request(
279 method="GET", path="/users",
280 headers={"Authorization": "Bearer wrong"},
281 ))
282 print(f" Status : {resp.status_code}")
283 print(f" Body : {resp.body}")
284 print(f" Headers: {resp.headers}\n")
285
286 # ── Test 4: Health check (zero-arg handler via adapter) ────
287 print("=" * 60)
288 print("TEST 4: GET /health (zero-arg handler, adapter figures it out)")
289 print("=" * 60)
290 resp = app.handle_request(Request(
291 method="GET", path="/health",
292 headers={"Authorization": "Bearer secret-123"},
293 ))
294 print(f" Status : {resp.status_code}")
295 print(f" Body : {resp.body}")
296 print(f" Headers: {resp.headers}\n")
297
298 # ── Test 5: Unknown route (404) ────────────────────────────
299 print("=" * 60)
300 print("TEST 5: GET /unknown (no matching route)")
301 print("=" * 60)
302 resp = app.handle_request(Request(
303 method="GET", path="/unknown",
304 headers={"Authorization": "Bearer secret-123"},
305 ))
306 print(f" Status : {resp.status_code}")
307 print(f" Body : {resp.body}")
308 print(f" Headers: {resp.headers}\n")
309
310 # ── Test 6: Switch view resolver to plain text ─────────────
311 print("=" * 60)
312 print("TEST 6: Same routes, but with PlainTextViewResolver")
313 print("=" * 60)
314 app._view_resolver = PlainTextViewResolver()
315 resp = app.handle_request(Request(
316 method="GET", path="/users/7",
317 headers={"Authorization": "Bearer admin-456"},
318 ))
319 print(f" Status : {resp.status_code}")
320 print(f" Body : {resp.body}")
321 print(f" Headers: {resp.headers}\n")
322
323 print("Front controller dispatched all requests through one entry point.")
324 print("Router matched patterns, adapter normalized signatures, resolver formatted output.")Common Mistakes
- ✗One endpoint class per URL instead of a central router. This works for five routes. At fifty, you have fifty files with duplicated setup logic and no way to see the full URL space at a glance.
- ✗Hardcoding the response format so every handler builds its own JSON string. When you need XML or HTML, you rewrite every handler instead of swapping in a different ViewResolver.
- ✗Tangling routing logic with business logic. The function that matches /users/:id should not also query the database. Separation means you can test routing and business logic independently.
- ✗Applying middleware globally when it should be per-route. Auth on the health check endpoint means your load balancer needs credentials. Route-scoped middleware fixes this.
Key Points
- ✓Front Controller is the single entry point for every request. One object coordinates routing, middleware, and response rendering instead of scattering that logic across many servlets or handler files.
- ✓Router uses pattern matching with path parameter extraction. The pattern /users/:id becomes a regex that matches /users/42 and pulls out id=42 as a named parameter.
- ✓HandlerAdapter lets you register any function shape without forcing everything to implement one rigid interface. A simple health check taking zero args and a full handler taking request plus params both work through the same dispatch path.
- ✓Middleware runs before and after the handler in a chain. Auth middleware can reject a request before it ever reaches the router. Logging middleware wraps the entire lifecycle to capture timing.