diff --git a/.doctrees/api.doctree b/.doctrees/api.doctree index cd61576..78ecd06 100644 Binary files a/.doctrees/api.doctree and b/.doctrees/api.doctree differ diff --git a/.doctrees/backlog.doctree b/.doctrees/backlog.doctree index 91d51b9..0c0e62d 100644 Binary files a/.doctrees/backlog.doctree and b/.doctrees/backlog.doctree differ diff --git a/.doctrees/cli.doctree b/.doctrees/cli.doctree index 1b169ab..4f61da6 100644 Binary files a/.doctrees/cli.doctree and b/.doctrees/cli.doctree differ diff --git a/.doctrees/deployment.doctree b/.doctrees/deployment.doctree index 6675cb5..b4e65bf 100644 Binary files a/.doctrees/deployment.doctree and b/.doctrees/deployment.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index 4a37ea7..0bf0c27 100644 Binary files a/.doctrees/environment.pickle and b/.doctrees/environment.pickle differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree index aaba530..7d2b673 100644 Binary files a/.doctrees/index.doctree and b/.doctrees/index.doctree differ diff --git a/.doctrees/testing.doctree b/.doctrees/testing.doctree index e8ca6e2..030b70a 100644 Binary files a/.doctrees/testing.doctree and b/.doctrees/testing.doctree differ diff --git a/.doctrees/tour.doctree b/.doctrees/tour.doctree index 8e731dc..091defc 100644 Binary files a/.doctrees/tour.doctree and b/.doctrees/tour.doctree differ diff --git a/.doctrees/tutorial-middleware.doctree b/.doctrees/tutorial-middleware.doctree index 7d702be..f900d62 100644 Binary files a/.doctrees/tutorial-middleware.doctree and b/.doctrees/tutorial-middleware.doctree differ diff --git a/_sources/api.rst.txt b/_sources/api.rst.txt index 7bc0506..72e5ee5 100644 --- a/_sources/api.rst.txt +++ b/_sources/api.rst.txt @@ -12,6 +12,20 @@ The central object of every Responder application. It holds your routes, middleware, templates, and configuration. Create one at the top of your module and use it to define your entire web service. +Quick example:: + + import responder + + api = responder.API( + title="My Service", # OpenAPI title + version="1.0", # OpenAPI version + openapi="3.0.2", # enable OpenAPI + docs_route="/docs", # Swagger UI at /docs + cors=True, # enable CORS + secret_key="change-me", # session signing key + allowed_hosts=["example.com"], + ) + .. module:: responder .. autoclass:: API @@ -28,6 +42,27 @@ parameters, the request body, cookies, and more. Most properties are synchronous, but reading the body requires ``await`` because it involves I/O. +Common patterns:: + + # Headers (case-insensitive) + token = req.headers.get("Authorization") + + # Query parameters: /search?q=python&page=2 + query = req.params["q"] + + # JSON body + data = await req.media() + + # Form data + form = await req.media("form") + + # File uploads + files = await req.media("files") + + # Client info + ip, port = req.client + is_https = req.is_secure + .. autoclass:: Request :inherited-members: @@ -39,6 +74,19 @@ The response object is passed into every view as the second argument. Mutate it to control what gets sent back to the client — the body, status code, headers, and cookies. +Common patterns:: + + resp.text = "plain text" # text/plain + resp.html = "
The central object of every Responder application. It holds your routes, middleware, templates, and configuration. Create one at the top of your module and use it to define your entire web service.
+Quick example:
+import responder
+
+api = responder.API(
+ title="My Service", # OpenAPI title
+ version="1.0", # OpenAPI version
+ openapi="3.0.2", # enable OpenAPI
+ docs_route="/docs", # Swagger UI at /docs
+ cors=True, # enable CORS
+ secret_key="change-me", # session signing key
+ allowed_hosts=["example.com"],
+)
+Most properties are synchronous, but reading the body requires await
because it involves I/O.
Common patterns:
+# Headers (case-insensitive)
+token = req.headers.get("Authorization")
+
+# Query parameters: /search?q=python&page=2
+query = req.params["q"]
+
+# JSON body
+data = await req.media()
+
+# Form data
+form = await req.media("form")
+
+# File uploads
+files = await req.media("files")
+
+# Client info
+ip, port = req.client
+is_https = req.is_secure
+The response object is passed into every view as the second argument. Mutate it to control what gets sent back to the client — the body, status code, headers, and cookies.
+Common patterns:
+resp.text = "plain text" # text/plain
+resp.html = "<h1>Hello</h1>" # text/html
+resp.media = {"key": "value"} # application/json
+resp.content = b"raw bytes" # application/octet-stream
+resp.file("path/to/file.pdf") # auto content-type
+resp.stream_file("large/export.csv") # streamed
+
+resp.status_code = 201
+resp.headers["X-Custom"] = "value"
+resp.cookies["session"] = "abc123"
+Group related routes under a shared URL prefix — useful for API versioning -and organizing large applications.
+and organizing large applications: +v1 = api.group("/v1")
+
+@v1.route("/users")
+def list_users(req, resp):
+ resp.media = []
+Run tasks in background threads without blocking the response. Available
-as api.background.
api.background:
+@api.route("/submit")
+async def submit(req, resp):
+ data = await req.media()
+
+ @api.background.task
+ def process(data):
+ # runs in a thread pool
+ ...
+
+ process(data)
+ resp.media = {"status": "accepted"}
+A dictionary subclass for query string parameters with multi-value support.
+A dictionary subclass for query string parameters with multi-value support.
+Behaves like a normal dict for single values, but supports getlist()
+for parameters that appear multiple times (e.g. ?tag=a&tag=b).
In-memory token bucket rate limiter. Limits requests per client IP address
-and returns 429 Too Many Requests when exceeded.
429 Too Many Requests when exceeded:
+from responder.ext.ratelimit import RateLimiter
+
+limiter = RateLimiter(requests=100, period=60) # 100 req/min
+limiter.install(api)
+Response headers: X-RateLimit-Limit, X-RateLimit-Remaining,
+and Retry-After (when limited).
429
Status Code Helpers¶
Convenience functions for checking which category a status code falls
-into. Useful in middleware and after-request hooks.
+into. Useful in middleware and after-request hooks:
+from responder.status_codes import is_200, is_400, is_500
+
+@api.after_request()
+def log_errors(req, resp):
+ if is_400(resp.status_code) or is_500(resp.status_code):
+ print(f"Error: {req.method} {req.url.path} -> {resp.status_code}")
+
+
-
responder.status_codes.is_100(status_code)[source]¶
diff --git a/backlog.html b/backlog.html
index affdd9d..646eef9 100644
--- a/backlog.html
+++ b/backlog.html
@@ -44,10 +44,11 @@
Future Ideas¶
-Consider adding after_request hooks (complement to before_request)
-Explore WebSocket before_request short-circuit support
-Add rate limiting middleware
-Consider async template rendering by default
+WebSocket before_request short-circuit support (reject before accept)
+Per-route rate limiting (different limits for different endpoints)
+Built-in structured logging with request context
+OpenAPI 3.1 support
+HTTP/2 server push
Responder automatically reads the PORT environment variable at
+runtime:
PORT — bind to 0.0.0.0 on this port (cloud platform convention)
When PORT is set, the server binds to all interfaces automatically.
+This is how cloud platforms like Fly.io, Railway, and Heroku inject the
+listen port.
For other settings like SECRET_KEY, read them in your application
+code and pass them to responder.API():
import os
+api = responder.API(secret_key=os.environ["SECRET_KEY"])
+If your project includes a JavaScript frontend with a package.json,
@@ -145,6 +162,7 @@ $ responder build /path/to/frontend
The pattern is always the same: deploy your code, set the start command
to python api.py, and the platform handles the rest.
Every production deployment needs a health check — a lightweight endpoint +that monitoring tools, load balancers, and orchestrators can poll to verify +your service is running:
+@api.route("/health")
+def health(req, resp):
+ resp.media = {"status": "healthy"}
+Keep it simple. Don’t query the database or do expensive work — the health +check should return instantly. Cloud platforms, Docker, and Kubernetes all +look for an HTTP 200 to confirm your service is alive.
+For Docker, add a HEALTHCHECK instruction:
HEALTHCHECK --interval=30s --timeout=3s \
+ CMD curl -f http://localhost/health || exit 1
+For production deployments where you want more control, bypass @@ -111,6 +130,43 @@ independently. A good starting point is 2-4 workers per CPU core.
Uvicorn supports many options — SSL certificates, access logging, graceful shutdown timeouts, and more. See the uvicorn documentation for details.
+For platforms like Heroku or Railway that use a Procfile:
web: uvicorn api:api --host 0.0.0.0 --port $PORT --workers 4
+For local development with databases and other services, Docker Compose +ties everything together:
+# docker-compose.yml
+services:
+ api:
+ build: .
+ ports:
+ - "5042:80"
+ environment:
+ - PORT=80
+ - DATABASE_URL=postgresql+asyncpg://user:pass@db/myapp
+ - SECRET_KEY=dev-secret
+ depends_on:
+ - db
+
+ db:
+ image: docker.io/postgres:16-alpine
+ environment:
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: pass
+ POSTGRES_DB: myapp
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+
+volumes:
+ pgdata:
+Run with docker compose up. The API waits for db to start, then
+connects using the DATABASE_URL environment variable.
Static asset serving — offload static files to the proxy
Rate limiting — at the infrastructure level
A minimal Caddy config that handles HTTPS automatically:
+# Caddyfile
+example.com {
+ reverse_proxy localhost:5042
+}
+Responder’s TrustedHostMiddleware and HTTPSRedirectMiddleware work
correctly behind proxies that set standard forwarding headers
(X-Forwarded-For, X-Forwarded-Proto).
That said, uvicorn is production-ready on its own. Many applications run uvicorn directly without a reverse proxy and do just fine.
Before going live:
+Set a secret key — SECRET_KEY env var, never the default
Disable debug mode — DEBUG=false or omit it entirely
Set allowed hosts — restrict to your actual domain names
Use multiple workers — --workers 4 or more, depending on CPU cores
Add a health check — /health endpoint for monitoring
Enable HTTPS — via your proxy, cloud platform, or uvicorn’s --ssl-* flags
Set up logging — uvicorn logs requests by default; pipe them to your log aggregator
Pin your dependencies — commit uv.lock for reproducible deploys
One pip install, batteries included:
Pydantic request validation and response serialization.
Mount Flask, Django, or any WSGI/ASGI app at a subroute.
Gzip compression, HSTS, CORS, and trusted host validation.
Before-request hooks that can short-circuit for auth guards.
Before-request and after-request hooks for auth and logging.
A test client for fast, in-process testing with pytest.
Route parameters with f-string syntax and type convertors.
Lifespan context managers for startup and shutdown logic.
Custom exception handlers for clean error responses.
GraphQL with Graphene and a built-in GraphiQL IDE.
Server-Sent Events for real-time streaming.
File serving with automatic content-type detection.
Sync and async views — async is always optional.
Class-based views with on_get, on_post, on_request.
Built-in rate limiting with X-RateLimit headers.
Content negotiation: JSON, YAML, and MessagePack.
A pleasant API with a single import statement.
OpenAPI schema generation with Swagger UI.
A production uvicorn server, ready to deploy.
HTTP method filtering for REST APIs.
Route groups for API versioning.
Signed cookie-based sessions.
Background tasks in a thread pool.
WebSocket support.
Rate limiters are just hooks — they run automatically during tests. +Verify the headers and the 429 response:
+from responder.ext.ratelimit import RateLimiter
+
+def test_rate_limiting():
+ api = responder.API(allowed_hosts=["localhost"])
+ limiter = RateLimiter(requests=2, period=60)
+ limiter.install(api)
+
+ @api.route("/")
+ def view(req, resp):
+ resp.text = "ok"
+
+ # First two requests succeed
+ for _ in range(2):
+ r = api.requests.get("http://localhost/")
+ assert r.status_code == 200
+ assert "X-RateLimit-Remaining" in r.headers
+
+ # Third request is rate limited
+ r = api.requests.get("http://localhost/")
+ assert r.status_code == 429
+When testing WSGI apps mounted at a subroute, use localhost as the
+host to avoid Werkzeug’s trusted host validation:
from flask import Flask
+
+def test_flask_mount():
+ api = responder.API(allowed_hosts=["localhost"])
+
+ flask_app = Flask(__name__)
+ @flask_app.route("/")
+ def hello():
+ return "Hello from Flask!"
+
+ api.mount("/flask", flask_app)
+
+ r = api.requests.get("http://localhost/flask")
+ assert r.status_code == 200
+ assert "Hello from Flask" in r.text
+Test the contract, not the implementation. Assert on status codes, response bodies, and headers — not on internal state.
Use ``localhost`` for mounted WSGI apps. Werkzeug 3.1.7+ validates
+the Host header, so avoid synthetic hosts like ; in tests.
Ret
can pace themselves.
The rate limiter is per-client, keyed by IP address.
+
+Pydantic Validation¶
+Pydantic models integrate directly with
+Responder’s routing. Set request_model to validate incoming data and
+response_model to control the shape of outgoing data:
+from pydantic import BaseModel
+
+class ItemIn(BaseModel):
+ name: str
+ price: float
+
+class ItemOut(BaseModel):
+ id: int
+ name: str
+ price: float
+
+@api.route("/items", methods=["POST"],
+ request_model=ItemIn, response_model=ItemOut)
+async def create_item(req, resp):
+ data = await req.media()
+ resp.media = {"id": 1, **data}
+
+
+When request_model is set:
+
+Valid requests are parsed and the data is available via await req.media()
+Invalid requests get an automatic 422 Unprocessable Entity response
+with detailed error messages — you don’t write any validation code
+
+When response_model is set:
+
+The response is serialized through the model before being sent
+Extra fields are stripped automatically
+Type coercion happens at the boundary
+
+This is the recommended way to build validated REST APIs with Responder.
+See the Building a REST API for a complete walkthrough.
+
+
+Content Negotiation¶
+Responder automatically negotiates the response format based on the
+client’s Accept header. Set resp.media to a Python object and
+the right thing happens:
+
+Accept: application/json (default) → JSON
+Accept: application/x-yaml → YAML
+Accept: application/x-msgpack → MessagePack
+
+This means a single endpoint serves multiple formats without any
+conditional logic in your code:
+@api.route("/data")
+def data(req, resp):
+ resp.media = {"key": "value"}
+
+
+Clients get the format they ask for:
+$ curl http://localhost:5042/data
+{"key": "value"}
+
+$ curl -H "Accept: application/x-yaml" http://localhost:5042/data
+key: value
+
+
+
MessagePack¶
MessagePack is a binary serialization format
@@ -589,11 +653,15 @@ high-throughput APIs, IoT devices, and anywhere bandwidth matters.
Responder supports MessagePack alongside JSON and YAML:
# Decode a MessagePack request body
data = await req.media("msgpack")
+
+# Respond with MessagePack
+resp.media = {"result": [1, 2, 3]}
-Content negotiation works too — clients can send
+
Content negotiation works automatically — clients can send
Accept: application/x-msgpack to receive MessagePack responses
-instead of JSON.
+instead of JSON. You can also explicitly decode MessagePack request
+bodies by passing "msgpack" to req.media().
@@ -644,6 +712,8 @@ instead of JSON.
Trusted Hosts
Request ID
Rate Limiting
+Pydantic Validation
+Content Negotiation
MessagePack
diff --git a/tutorial-middleware.html b/tutorial-middleware.html
index bfc84c9..3a395de 100644
--- a/tutorial-middleware.html
+++ b/tutorial-middleware.html
@@ -145,6 +145,44 @@ and the response last.
the existing stack. Keep this in mind for ordering dependencies — if
middleware A depends on middleware B having run first, add B before A.
+
+Writing Pure ASGI Middleware¶
+For maximum performance and control, you can write middleware as a plain
+ASGI application. This bypasses Starlette’s BaseHTTPMiddleware
+abstraction — it’s faster and gives you direct access to the ASGI
+protocol:
+class SecurityHeadersMiddleware:
+ def __init__(self, app):
+ self.app = app
+
+ async def __call__(self, scope, receive, send):
+ if scope["type"] != "http":
+ await self.app(scope, receive, send)
+ return
+
+ async def send_with_headers(message):
+ if message["type"] == "http.response.start":
+ extra = [
+ (b"x-content-type-options", b"nosniff"),
+ (b"x-frame-options", b"DENY"),
+ (b"referrer-policy", b"strict-origin-when-cross-origin"),
+ ]
+ message["headers"] = list(message["headers"]) + extra
+ await send(message)
+
+ await self.app(scope, receive, send_with_headers)
+
+api.add_middleware(SecurityHeadersMiddleware)
+
+
+This is the same pattern used internally by Starlette and uvicorn. The
+middleware receives the ASGI scope, receive, and send callables,
+and wraps send to inject headers into the response.
+For most cases, BaseHTTPMiddleware is simpler and perfectly fine.
+Use the pure ASGI approach when you need to handle WebSocket connections,
+streaming responses, or want to avoid the overhead of request/response
+object creation.
+
When to Use What¶
@@ -189,6 +227,7 @@ middleware when hooks aren’t enough.
- Built-in Middleware
- Adding Third-Party Middleware
- Middleware Order
+- Writing Pure ASGI Middleware
- When to Use What