A small collection of reusable middlewares for FastAPI: request logging, rate limiting and response standardization.
Project description
midkit
English | 한국어
A small, dependency-light collection of reusable middlewares for FastAPI. No external runtime dependencies beyond FastAPI/Starlette — everything else is the standard library.
Features
| Middleware | What it does |
|---|---|
RequestLoggerMiddleware |
Logs method, path, status code and elapsed time (ms) for each request. |
RateLimitMiddleware |
IP-based sliding-window rate limiting with 429 + rate-limit headers. |
ResponseWrapperMiddleware |
Wraps JSON responses in a standard {success, data, error} envelope. |
Installation
pip install midkit
Quick Start
from fastapi import FastAPI
from midkit import (
RequestLoggerMiddleware,
RateLimitMiddleware,
ResponseWrapperMiddleware,
)
app = FastAPI()
# 추가 순서가 곧 적용 순서: 나중에 추가한 것이 바깥쪽(요청을 먼저 받음).
# ResponseWrapper 를 먼저 추가해 가장 안쪽에서 응답을 먼저 감싼다.
app.add_middleware(ResponseWrapperMiddleware)
app.add_middleware(RateLimitMiddleware, max_requests=100, window_seconds=60)
app.add_middleware(RequestLoggerMiddleware, exclude_paths=["/health"])
@app.get("/hello")
def hello():
return {"msg": "hi"}
Ordering matters. FastAPI wraps middleware as an onion: the middleware added last sits on the outside. Add
ResponseWrapperMiddlewarefirst so it is the innermost layer and wraps the handler's raw output before the others run. Seeexamples/basic_usage.py.
All three middlewares share an exclude_paths option that accepts exact paths,
prefixes (/api) and shell-style globs (/static/*).
RequestLoggerMiddleware
Logs one line per request. 4xx/5xx responses are logged at WARNING, everything
else at INFO. The logger name is midkit.logger.
| Option | Type | Default | Description |
|---|---|---|---|
exclude_paths |
list[str] |
[] |
Paths that should not be logged. |
log_body |
bool |
False |
Log up to the first 200 chars of the request body. |
logger_instance |
Logger | None |
None |
Custom logger; defaults to the library logger. |
app.add_middleware(
RequestLoggerMiddleware,
exclude_paths=["/health", "/metrics"],
log_body=True,
)
Example log line:
INFO midkit.logger GET /hello -> 200 (1.23ms)
Configure logging to see the output. The logger emits
INFOfor 2xx/3xx andWARNINGfor 4xx/5xx, but Python's root logger defaults toWARNINGand has no handler attached — so by default only the 4xx/5xx lines appear (or none at all). Set up logging once at startup to see every request:import logging # 모든 요청(INFO 포함)을 보려면 레벨을 INFO 로 낮춘다 logging.basicConfig(level=logging.INFO)Already running under Uvicorn/Gunicorn? They configure their own handlers, so you usually only need to raise the level for the midkit logger specifically:
logging.getLogger("midkit.logger").setLevel(logging.INFO)
RateLimitMiddleware
Sliding-window limiter keyed per client. Timestamps are stored in a deque and
pruned on each request.
| Option | Type | Default | Description |
|---|---|---|---|
max_requests |
int |
100 |
Max requests allowed per window. |
window_seconds |
int |
60 |
Window length in seconds. |
exclude_paths |
list[str] |
[] |
Paths exempt from rate limiting. |
key_func |
Callable[[Request], str] | None |
None |
Custom client key; defaults to client IP. |
By default the client key is the first IP in X-Forwarded-For, falling back to
request.client.host.
Every response carries:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
When the limit is exceeded the request is rejected with 429:
{
"error": "rate_limit_exceeded",
"detail": "Rate limit of 100 requests per 60s exceeded."
}
and a Retry-After header indicating when to try again.
ResponseWrapperMiddleware
Wraps application/json responses in a standard envelope. Non-JSON responses
(HTML, files, streams) pass through untouched, and /docs, /redoc and
/openapi.json are excluded by default.
| Option | Type | Default | Description |
|---|---|---|---|
exclude_paths |
list[str] |
[] |
Extra paths returned without wrapping. |
wrap_errors |
bool |
True |
Also wrap 4xx/5xx responses. |
Success response:
{ "success": true, "data": { "id": 1, "name": "widget" }, "error": null }
Error response (4xx/5xx):
{ "success": false, "data": null, "error": { "code": 404, "message": "Not Found" } }
Responses that already contain the success/data/error keys are passed
through as-is to avoid double wrapping.
Usage Examples
A complete, runnable app combining all three middlewares lives in
examples/basic_usage.py. Run it with:
pip install -e ".[dev]" uvicorn
uvicorn examples.basic_usage:app --reload
It applies the middlewares like this (note the ordering):
# 추가 순서가 곧 적용 순서: 나중에 추가한 것이 바깥쪽.
app.add_middleware(ResponseWrapperMiddleware, exclude_paths=["/raw"])
app.add_middleware(RateLimitMiddleware, max_requests=10, window_seconds=60,
exclude_paths=["/health"])
app.add_middleware(RequestLoggerMiddleware, exclude_paths=["/health"], log_body=True)
Successful response (wrapped + rate-limit headers)
$ curl -i http://127.0.0.1:8000/users/1
HTTP/1.1 200 OK
x-ratelimit-limit: 10
x-ratelimit-remaining: 9
{"success":true,"data":{"id":1,"name":"user-1"},"error":null}
Error response (wrapped too)
$ curl -i http://127.0.0.1:8000/users/0
HTTP/1.1 404 Not Found
{"success":false,"data":null,"error":{"code":404,"message":"User not found"}}
Excluded path (returned raw, not wrapped)
$ curl -s http://127.0.0.1:8000/raw
{"raw":true}
Rate limit exceeded
# Hammer the endpoint past max_requests=10
$ for i in $(seq 1 12); do
curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1:8000/users/1
done
200 200 200 200 200 200 200 200 200 200 429 429
$ curl -i http://127.0.0.1:8000/users/1
HTTP/1.1 429 Too Many Requests
retry-after: 42
x-ratelimit-limit: 10
x-ratelimit-remaining: 0
{"error":"rate_limit_exceeded","detail":"Rate limit of 10 requests per 60s exceeded."}
Request log output
With logging configured (see the note above), each request prints one line:
INFO midkit.logger GET /users/1 -> 200 (0.40ms)
WARNING midkit.logger GET /users/0 -> 404 (0.31ms)
Development
pip install -e ".[dev]"
pytest tests/ -v
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file midkit-0.1.0.tar.gz.
File metadata
- Download URL: midkit-0.1.0.tar.gz
- Upload date:
- Size: 13.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4ca7679a73b9d8cce170fb394f647887b14c812c5bced22d5adfd4306a41832
|
|
| MD5 |
2706ff837ee2fa9f4faecb433e994b22
|
|
| BLAKE2b-256 |
f7ca54972efd2acb3429fba07192d0527d58a28d42a629070083fc967b048c9b
|
File details
Details for the file midkit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: midkit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6070c14d4fe0990ff39430e6b07950e2135f6f5684d1ca24a6f1fab0a57823ed
|
|
| MD5 |
6a580eaf315cad635fc5f94a2fb1e70d
|
|
| BLAKE2b-256 |
ef88bea5d774eb0ed41df7fca34a2197e72fe97bfc2b0e656682cb42f874170a
|