Skip to main content

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 ResponseWrapperMiddleware first so it is the innermost layer and wraps the handler's raw output before the others run. See examples/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 INFO for 2xx/3xx and WARNING for 4xx/5xx, but Python's root logger defaults to WARNING and 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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

midkit-0.1.0.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

midkit-0.1.0-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

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

Hashes for midkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d4ca7679a73b9d8cce170fb394f647887b14c812c5bced22d5adfd4306a41832
MD5 2706ff837ee2fa9f4faecb433e994b22
BLAKE2b-256 f7ca54972efd2acb3429fba07192d0527d58a28d42a629070083fc967b048c9b

See more details on using hashes here.

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

Hashes for midkit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6070c14d4fe0990ff39430e6b07950e2135f6f5684d1ca24a6f1fab0a57823ed
MD5 6a580eaf315cad635fc5f94a2fb1e70d
BLAKE2b-256 ef88bea5d774eb0ed41df7fca34a2197e72fe97bfc2b0e656682cb42f874170a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page