Middleware and errors · Backend Development
How do you add logging and error handling to every route without repeating yourself?
Course contentsWhy you need it
Every handler needs the same extras: log the request, time it, turn crashes into a clean 500, check the login. Copying that into fifty handlers is how bugs and gaps appear. Middleware wraps handlers once, and every framework has it: decorators in Flask and FastAPI, app.use in Express, filters in Spring.
The intuition
A decorator is a function that takes a handler and returns a new handler that does something before and after calling the original:
def logged(handler):
def wrapper(request):
print("->", request["path"])
response = handler(request)
print("<-", response[0])
return response
return wrapper
@logged
def home(request):
return 200, "welcome"@logged above def home means home = logged(home). Calling home(...) now runs wrapper, which calls the original.
Stack them and they nest like layers of an onion: with @a above @b, a request goes through a, then b, then the handler, and the response comes back out through b, then a.
An error middleware catches any exception, logs the details for you, and sends the client a plain 500 {"error": "internal error"}. Never send the exception text or a stack trace to the client: it can reveal file paths, SQL and secrets.
Definition
Middleware
Code that runs around every handler: logging, timing, auth, error handling.
Decorator
A function that takes a function and returns a wrapped version. @name above def applies it.
Examples
A logging decorator
Try it yourselfThe wrapper runs around the handler.
def logged(handler):
def wrapper(request):
print("->", request["path"])
response = handler(request)
print("<-", response[0])
return response
return wrapper
@logged
def home(request):
return 200, "welcome"
result = home({"path": "/"})
print(result)-> / <- 200 (200, 'welcome')
- L9Same as writing home = logged(home) after the def.
- L13This calls wrapper, not the original home.
- L3Before the handler.
-> / - L4The original handler runs here.
- L5After the handler.
<- 200
Layers of an onion
Try it yourselfTwo decorators: in through the outer one, out through it last.
def layer(name):
def decorate(handler):
def wrapper(request):
print("in", name)
response = handler(request)
print("out", name)
return response
return wrapper
return decorate
@layer("A")
@layer("B")
def hello(request):
print("handler")
return 200, "hi"
hello({})in A in B handler out B out A
- L1A decorator factory: layer("A") returns a decorator.
- L11Applied last, so it's the outermost layer.
- L17in A, in B, handler, out B, out A.
Errors become a clean 500
Try it yourselfThe client gets a plain message; the details go to your log.
LOG = []
def catch_errors(handler):
def wrapper(request):
try:
return handler(request)
except Exception as e:
LOG.append(f"{request['path']}: {type(e).__name__}: {e}")
return 500, {"error": "internal error"}
return wrapper
@catch_errors
def average(request):
nums = request["nums"]
return 200, {"avg": sum(nums) / len(nums)}
print(average({"path": "/avg", "nums": [2, 4]}))
print(average({"path": "/avg", "nums": []}))
print(LOG)(200, {'avg': 3.0})
(500, {'error': 'internal error'})
['/avg: ZeroDivisionError: division by zero']
- L7Any crash inside the handler lands here.
- L8Keep the details for the developers.
- L9The client learns only that something went wrong.
- L18Dividing by len([]) = 0 raises ZeroDivisionError, which becomes a 500.
Common mistakes
Forgetting to return the response
A wrapper that calls handler(request) but doesn't return it makes every route return None.
Leaking error details
return 500, {"error": str(e)} can show SQL, file paths or secrets to anyone. Log the details; send a generic message.
Decorator order
The top decorator is the outermost. Put error handling outside logging if you want crashes logged with their status.
Quick quiz
1. What does `@logged` above `def home` do?
Show the answer
Answer: A
A decorator replaces the function with whatever the decorator returns.
2. What does this print?
def shout(handler):
def wrapper(x):
return handler(x).upper()
return wrapper
def exclaim(handler):
def wrapper(x):
return handler(x) + "!"
return wrapper
@shout
@exclaim
def greet(name):
return "hi " + name
print(greet("ann"))Show the answer
Answer: A
exclaim wraps greet first ("hi ann!"), then shout uppercases the whole thing.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictFollow the layers
Type exactly what this prints.
- ModifyStop leaking errors
This error middleware sends the exception text to the client. Change it so the client gets
500{"error": "internal error"}, and the details"<ExceptionType>: <message>"are appended toLOGinstead. - WriteRequire JSON
Write a decorator
require_json(handler). The wrapper receives arequestdict with"headers"and"body":- if
request["headers"].get("Content-Type")isn't"application/json"→ return(415, {"error": "expected JSON"}) - if the body isn't valid JSON → return
(400, {"error": "invalid JSON"}) - otherwise set
request["json"]to the parsed body and returnhandler(request)
- if
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| Middleware | 中间件 | Code that wraps every request: logging, auth, errors. |
| Decorator | 装饰器 | @name above a def: replaces the function with a wrapped version. |