Full docs pass: educational prose throughout all pages

Every section now teaches web development concepts alongside the code:
- HTTP methods, status codes, content negotiation explained
- What ASGI is and why it matters
- How cookies, sessions, CORS, and HSTS work
- When to use WebSockets vs SSE
- Why request validation matters
- How background tasks differ from task queues
- What rate limiting protects against
- What Host header injection is
- Separation of concerns in templating

People should learn about web development while reading these docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 13:07:56 -04:00
parent b3c55f68d9
commit 226bd63ed3
4 changed files with 471 additions and 267 deletions
+50 -33
View File
@@ -1,34 +1,32 @@
Deployment
==========
Responder applications are standard ASGI apps. You can deploy them anywhere
you'd deploy a Python web service.
Responder applications are standard `ASGI <https://asgi.readthedocs.io/>`_
apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor
to WSGI — it supports async, WebSockets, and HTTP/2. This means you can
deploy a Responder app anywhere that runs Python, using any ASGI server.
Running Locally
---------------
The simplest way to run your application::
# api.py
import responder
api = responder.API()
@api.route("/")
def hello(req, resp):
resp.text = "hello, world!"
During development, ``api.run()`` is all you need::
if __name__ == "__main__":
api.run()
This starts a production uvicorn server on ``127.0.0.1:5042``.
This starts a `uvicorn <https://www.uvicorn.org/>`_ server on
``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on
`uvloop <https://uvloop.readthedocs.io/>`_ — it handles thousands of
concurrent connections efficiently and protects against slowloris attacks,
making a reverse proxy like nginx optional for many deployments.
Docker
------
A minimal Dockerfile for deploying a Responder application::
Docker is the most common way to package and deploy web applications.
Here's a minimal Dockerfile::
FROM python:3.13-slim
WORKDIR /app
@@ -43,44 +41,63 @@ Build and run::
$ docker build -t myapi .
$ docker run -p 8000:80 myapi
The ``python:3.13-slim`` image is about 150MB — small enough for fast
deploys but includes everything you need. For even smaller images, you
can use ``python:3.13-alpine``, though some packages may need extra
build dependencies.
Cloud Platforms
---------------
Responder automatically honors the ``PORT`` environment variable, which is
set by most cloud platforms. When ``PORT`` is set, Responder binds to
``0.0.0.0`` on that port automatically.
Responder automatically honors the ``PORT`` environment variable. When
``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is
the convention that virtually every cloud platform uses.
This works out of the box with:
This means zero configuration on:
- **Fly.io**
- **Railway**
- **Render**
- **Google Cloud Run**
- **Azure Container Apps**
- **AWS App Runner**
- **Fly.io**``fly launch`` and you're done
- **Railway** — push your code, Railway sets ``PORT``
- **Render** — set start command to ``python api.py``
- **Google Cloud Run** — containerize and deploy
- **Azure Container Apps** — same pattern
- **AWS App Runner** — and here too
Just deploy your code and set the start command to ``python api.py``.
The pattern is always the same: deploy your code, set the start command
to ``python api.py``, and the platform handles the rest.
Uvicorn Directly
----------------
For more control over the production server, you can bypass ``api.run()``
and use uvicorn directly::
For production deployments where you want more control, bypass
``api.run()`` and use uvicorn directly::
$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
This gives you access to all of uvicorn's options: worker count, SSL
certificates, access logging, and more. See the
`uvicorn documentation <https://www.uvicorn.org/>`_ for details.
The ``--workers`` flag spawns multiple processes, each handling requests
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 <https://www.uvicorn.org/deployment/>`_ for details.
Reverse Proxy
-------------
In production, you may want to place Responder behind a reverse proxy like
nginx or Caddy for SSL termination, load balancing, or serving static assets.
For high-traffic production deployments, you may want a reverse proxy like
`nginx <https://nginx.org/>`_ or `Caddy <https://caddyserver.com/>`_ in
front of your application for:
- **SSL/TLS termination** — let the proxy handle HTTPS certificates
- **Load balancing** — distribute traffic across multiple app instances
- **Static asset serving** — offload static files to the proxy
- **Rate limiting** — at the infrastructure level
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers.
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.
+155 -57
View File
@@ -2,164 +2,229 @@ Quick Start
===========
This guide will walk you through the basics of building a web service with
Responder. By the end, you'll know how to declare routes, handle requests,
send responses, render templates, and process background tasks.
Responder. By the end, you'll understand how HTTP requests and responses
work, how to define routes, read data from clients, send data back, render
HTML templates, and process work in the background.
Create a Web Service
--------------------
The first thing you need to do is declare a web service. This is the central
object that holds all your routes, middleware, and configuration::
Every web application starts with a single object — the application
instance. In Responder, this is the ``API`` class. It holds your routes,
middleware, templates, and configuration. Think of it as the central
nervous system of your web service::
import responder
api = responder.API()
That's it. One import, one line. You now have a fully functional ASGI
application with gzip compression, static file serving, session support,
and a production-ready server — all wired up and ready to go.
Hello World
-----------
Next, add a route. Here, we'll make the root URL say "hello, world!"::
A web service isn't very useful until it can respond to requests. In HTTP,
a *route* maps a URL path to a function that handles it. When a client
(like a browser or ``curl``) sends a request to that path, your function
runs and produces a response.
Here's the simplest possible route::
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
Every view receives a ``req`` (request) and ``resp`` (response) object. You
don't need to return anything — just mutate the response directly.
Two things to notice:
1. Every view function receives two arguments: ``req`` (the incoming
request) and ``resp`` (the outgoing response).
2. You don't return anything. Instead, you *mutate* the response object
directly. This is a deliberate design choice — it keeps the API
consistent whether you're setting text, JSON, headers, cookies, or
status codes.
Run the Server
--------------
Start your web service with ``api.run()``::
Start your web service with a single call::
api.run()
This spins up a production-grade uvicorn server on port ``5042``, ready for
incoming HTTP requests.
This spins up a production-grade `uvicorn <https://www.uvicorn.org/>`_
server on port ``5042``, ready for incoming HTTP requests. Open
``http://localhost:5042`` in your browser and you'll see your hello world
response.
You can customize the port with ``api.run(port=8000)``. The ``PORT``
environment variable is also honored automatically — when set, Responder
binds to ``0.0.0.0`` on that port, which is what cloud platforms like
Fly.io, Railway, and Google Cloud Run expect.
binds to ``0.0.0.0`` on that port, which is what cloud platforms expect.
.. note::
Both sync and async views are supported. The ``async`` keyword is always
optional — use it when you need to ``await`` something.
optional — use it when you need to ``await`` something, like reading a
request body or querying a database.
Route Parameters
----------------
If you want dynamic URLs, use Python's familiar f-string syntax to declare
variables in your routes::
Static URLs like ``/about`` are useful, but most applications need dynamic
routes — URLs that contain variable data, like a user ID or a product slug.
In Responder, you declare route parameters using Python's f-string syntax::
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``.
A request to ``/hello/guido`` will respond with ``hello, guido!``.
Route parameters are passed as keyword-only arguments (after the ``*``).
Route parameters are passed as *keyword-only* arguments (after the ``*``
in the function signature). This is a Python feature that makes the
interface explicit — you always know which arguments come from the URL.
Type Convertors
^^^^^^^^^^^^^^^
You can constrain route parameters to specific types. The parameter will be
automatically converted before it reaches your view::
By default, route parameters are strings. But often you want them as
integers, UUIDs, or other types. Responder can convert them automatically
using type annotations in the route pattern::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings.
If someone requests ``/add/3/hello``, they'll get a 404 — the route won't
match because ``hello`` isn't a valid integer.
Supported types:
- ``str`` — matches any string without slashes (default)
- ``int`` — matches digits, converts to ``int``
- ``float`` — matches decimal numbers, converts to ``float``
- ``str`` — matches any string without slashes (this is the default)
- ``int`` — matches digits and converts to ``int``
- ``float`` — matches decimal numbers and converts to ``float``
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
- ``path`` — matches any string *including* slashes, useful for file paths
like ``/files/{filepath:path}``
Sending Responses
-----------------
Responder gives you several ways to send data back to the client. Just set
the appropriate property on the response object.
When an HTTP server receives a request, it must send back a response. Every
HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not
Found``), headers (metadata like ``Content-Type``), and a body (the actual
data).
**Text and HTML**::
Responder lets you set all three by mutating the response object.
**Text and HTML** — the simplest response types. ``resp.text`` sets the
``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to
``text/html``::
resp.text = "plain text response"
resp.html = "<h1>HTML response</h1>"
**JSON** — the most common pattern for APIs. Set ``resp.media`` to any
JSON-serializable Python object::
**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any
JSON-serializable Python object — a dict, a list, whatever — and Responder
will serialize it to JSON and set the right headers::
@api.route("/hello/{who}/json")
def hello_json(req, resp, *, who):
resp.media = {"hello": who}
If the client sends an ``Accept: application/x-yaml`` header, the same data
will be returned as YAML instead. Content negotiation is automatic.
will be returned as YAML instead. This is called *content negotiation*
the server and client agree on a format. It happens automatically.
**Files** — serve a file from disk with automatic content-type detection::
**Files** — serve a file from disk. Responder uses Python's ``mimetypes``
module to figure out the ``Content-Type`` from the file extension::
resp.file("reports/annual.pdf")
**Raw bytes**::
**Raw bytes** — for binary data like images or protocol buffers::
resp.content = b"\x89PNG\r\n..."
**Status codes and headers**::
**Status codes** — HTTP status codes tell the client what happened. ``200``
means success, ``201`` means something was created, ``404`` means not found,
``500`` means the server broke. Set it directly::
resp.status_code = 201
**Headers** — HTTP headers carry metadata. Common ones include
``Content-Type``, ``Cache-Control``, ``Authorization``, and custom
application headers::
resp.headers["X-Custom"] = "value"
**Redirects**::
**Redirects** — tell the client to go somewhere else::
api.redirect(resp, location="/new-url")
This sends a ``301 Moved Permanently`` response by default. The client's
browser will automatically follow the redirect.
Reading Requests
----------------
The request object gives you access to everything the client sent.
The other half of HTTP is the request — the data the client sends to your
server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL,
headers, query parameters, cookies, and optionally a body.
**Method and URL**::
Responder wraps all of this in the ``req`` object.
**Method and URL** — every HTTP request has a method (what the client wants
to do) and a URL (what resource it's about)::
req.method # "get", "post", etc. (lowercase)
req.full_url # "http://example.com/path?q=1"
req.url # parsed URL object
**Headers**case-insensitive, just like you'd expect::
**Headers**HTTP headers carry metadata from the client, like what
content types it accepts, authentication tokens, and more. Responder's
headers dict is case-insensitive, because the HTTP spec says header names
are case-insensitive::
req.headers["Content-Type"]
req.headers["content-type"] # same thing
**Query parameters**::
**Query parameters** — the part of the URL after the ``?``. These are
commonly used for search, filtering, and pagination::
# GET /search?q=python&page=2
req.params["q"] # "python"
req.params["page"] # "2"
**Path parameters** — also available on the request object::
Note that query parameters are always strings. If you need an integer,
you'll need to convert it yourself: ``int(req.params["page"])``.
**Path parameters** — the dynamic parts of the URL that matched your route
pattern. These are also available on the request object, which is useful
in before-request hooks where they aren't passed as function arguments::
req.path_params["user_id"] # same as the keyword argument
**Request body** — for POST/PUT/PATCH requests, you need to ``await`` the
body content::
**Request body** — for POST, PUT, and PATCH requests, the client sends
data in the body. Since reading the body is an I/O operation, you need to
``await`` it::
# JSON body
# JSON body (the most common format for APIs)
data = await req.media()
# Form data
# Form data (from HTML forms)
data = await req.media("form")
# File uploads
# File uploads (multipart)
files = await req.media("files")
# Raw bytes
@@ -170,41 +235,59 @@ body content::
**Other useful properties**::
req.is_json # True if content type is JSON
req.cookies # dict of cookies
req.session # session data (dict)
req.client # (host, port) tuple
req.is_secure # True if HTTPS
req.is_json # True if the content type is JSON
req.cookies # dict of cookies sent by the client
req.session # session data (a signed, server-side dict)
req.client # (host, port) tuple — the client's IP address
req.is_secure # True if the request came over HTTPS
Rendering Templates
-------------------
Responder includes built-in `Jinja2 <https://jinja.palletsprojects.com/>`_
support. Templates are loaded from the ``templates/`` directory by default.
While APIs typically return JSON, many web applications need to render
HTML pages. Responder includes built-in support for
`Jinja2 <https://jinja.palletsprojects.com/>`_, one of the most popular
templating engines in the Python ecosystem.
The simplest way is to use ``api.template()``::
Templates let you write HTML with placeholders that get filled in with
dynamic data. This keeps your presentation logic (HTML) separate from
your application logic (Python) — a pattern called
*separation of concerns*.
The simplest way to render a template is ``api.template()``. Templates
are loaded from the ``templates/`` directory by default::
@api.route("/hello/{name}/html")
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
You can also use the ``Templates`` class directly for more control::
The template file ``templates/hello.html`` might look like::
<h1>Hello, {{ name }}!</h1>
The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with
the value you passed in.
You can also use the ``Templates`` class directly for more control over
the template directory and configuration::
from responder.templates import Templates
templates = Templates(directory="templates")
templates = Templates(directory="my_templates")
@api.route("/page")
def page(req, resp):
resp.html = templates.render("page.html", title="Hello")
Async rendering is supported too::
For applications that need non-blocking template rendering (rare, but
useful under extreme load), async rendering is supported::
templates = Templates(directory="templates", enable_async=True)
resp.html = await templates.render_async("page.html", title="Hello")
You can render template strings without a file::
And for quick one-off templates, you can render a string directly without
a file::
resp.html = api.template_string("Hello, {{ name }}!", name="world")
@@ -213,7 +296,13 @@ Background Tasks
----------------
Sometimes you want to accept a request, respond immediately, and do the
actual processing later. Responder makes this easy with background tasks::
actual processing later. This is a common pattern for operations that take
a long time — sending emails, processing images, updating caches, or
calling slow external APIs.
Responder makes this easy with background tasks. Decorate any function
with ``@api.background.task`` and it will run in a thread pool, separate
from the request/response cycle::
@api.route("/incoming")
async def receive_incoming(req, resp):
@@ -227,8 +316,17 @@ actual processing later. Responder makes this easy with background tasks::
process_data(data)
# Respond immediately processing continues in the background
# This response is sent immediately, while process_data
# continues running in the background.
resp.media = {"status": "accepted"}
The ``@api.background.task`` decorator wraps any function to run in a thread
pool. The client gets an immediate response while the work continues.
The client gets an instant response — the heavy lifting happens after.
This is the same pattern used by task queues like Celery, but much simpler
for lightweight use cases where you don't need a full message broker.
.. note::
Background tasks run in threads, not processes. They share memory with
your application, which makes them fast to start but means CPU-intensive
work will block the event loop. For heavy computation, consider a proper
task queue.
+1 -1
View File
@@ -279,7 +279,7 @@ Tips
``API()`` configuration (like ``cors=True``), create a new instance
in the test rather than sharing the fixture.
- **Use ``api.url_for()``** instead of hard-coded paths. It's a small
- Use ``api.url_for()`` instead of hard-coded paths. It's a small
thing, but it makes refactoring painless.
- **Test the contract, not the implementation.** Assert on status codes,
+265 -176
View File
@@ -1,15 +1,27 @@
Feature Tour
============
This section walks through Responder's features in detail. Each section
includes working code examples you can copy into your application.
This section walks through Responder's features in depth. Each section
explains the concept, shows working code, and explains the design choices
behind it. If you're new to web development, this is a good place to learn
how modern web frameworks work under the hood.
Method Filtering
----------------
By default, a route matches all HTTP methods. If you want to restrict a
route to specific methods, pass the ``methods`` parameter::
HTTP defines several *methods* (also called verbs) that describe what a
client wants to do with a resource. The most common are:
- ``GET`` — retrieve data
- ``POST`` — create something new
- ``PUT`` — replace something entirely
- ``PATCH`` — update part of something
- ``DELETE`` — remove something
By default, a Responder route matches all methods. This is fine for simple
endpoints, but REST APIs typically map different methods to different
operations. Use the ``methods`` parameter to restrict a route::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
@@ -20,15 +32,22 @@ route to specific methods, pass the ``methods`` parameter::
data = await req.media()
resp.media = {"created": data}
Note the ``check_existing=False``this allows you to register multiple
handlers for the same path with different methods.
Note the ``check_existing=False``Responder normally prevents you from
registering two routes with the same path (to catch typos). When you
intentionally want multiple handlers for the same path with different
methods, you need to opt in.
Class-Based Views
-----------------
For more complex resources, you can use class-based views. Responder will
dispatch to the appropriate method handler based on the HTTP method::
Function-based views are great for simple endpoints, but sometimes you want
to group related HTTP methods together into a single resource. This is
where class-based views come in — a pattern popularized by
`Falcon <https://falconframework.org/>`_.
Responder dispatches to the appropriate method handler based on the HTTP
method::
@api.route("/{greeting}")
class GreetingResource:
@@ -47,14 +66,19 @@ middleware scoped to a single route. Method-specific handlers (``on_get``,
``on_post``, ``on_put``, ``on_delete``, etc.) are called after.
No inheritance required — just define a class with the right method names.
This is simpler than Django's ``View`` classes and more Pythonic than
framework-specific base classes.
Lifespan Events
---------------
Modern applications often need to set up resources on startup (database
connections, caches, ML models) and tear them down on shutdown. Responder
supports the lifespan context manager pattern::
Real applications need to set up resources when they start (database
connection pools, ML models, caches) and tear them down when they stop.
This is called the application *lifespan*.
The modern approach is the *context manager* pattern, where startup and
shutdown are two halves of the same block::
from contextlib import asynccontextmanager
@@ -68,7 +92,11 @@ supports the lifespan context manager pattern::
api = responder.API(lifespan=lifespan)
You can also use the traditional event decorator style::
Everything before ``yield`` runs at startup. Everything after runs at
shutdown. If startup fails, the server won't start. If shutdown raises,
it's logged but the server still exits.
The traditional event decorator style also works::
@api.on_event("startup")
async def startup():
@@ -78,57 +106,71 @@ You can also use the traditional event decorator style::
async def shutdown():
print("shutting down")
The context manager approach is preferred for new code — it makes the
startup/shutdown relationship explicit and keeps related code together.
The context manager is preferred for new code — it keeps related startup
and shutdown logic together and makes resource cleanup more explicit.
Serving Files
-------------
Serve files from disk with automatic content-type detection. Responder
uses Python's ``mimetypes`` module to figure out the right ``Content-Type``
header for you::
Web applications often need to serve files — downloads, reports, images.
Responder makes this simple with ``resp.file()``, which reads a file from
disk and sets the ``Content-Type`` header automatically using Python's
``mimetypes`` module::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can override the content type if needed::
You can override the content type if the automatic detection isn't right::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
For large files, use ``resp.stream_file()`` to avoid loading the entire
file into memory. This streams the file in chunks::
@api.route("/export")
def export(req, resp):
resp.stream_file("data/export.csv")
Custom Error Handling
---------------------
By default, unhandled exceptions result in a 500 Internal Server Error.
You can register custom handlers for specific exception types to return
structured error responses::
In production, you don't want your users to see raw Python tracebacks.
Responder lets you register custom handlers for specific exception types,
so you can return clean, structured error responses::
@api.exception_handler(ValueError)
async def handle_value_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
Now, any route that raises a ``ValueError`` will return a clean 400 response
with a JSON error message instead of a generic 500 page.
Now, any route that raises a ``ValueError`` will return a clean JSON
response with a 400 status code instead of a generic 500 error page.
This is a common pattern in API development — you define your own exception
classes for different error conditions, register handlers for each, and
your API always returns consistent, machine-readable error responses.
Before-Request Hooks
--------------------
Run code before every request. This is useful for logging, adding common
headers, or setting up per-request state::
Sometimes you need to run the same code before every request —
authentication checks, request logging, adding common headers, or setting
up per-request state. Before-request hooks let you do this without
duplicating code in every route::
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.1"
resp.headers["X-API-Version"] = "3.2"
**Short-circuiting:** If your hook sets ``resp.status_code``, the route
handler will be skipped entirely and the response will be sent immediately.
This is the pattern for authentication guards::
**Short-circuiting** is the really powerful part. If your hook sets
``resp.status_code``, the route handler is skipped entirely and the
response is sent immediately. This is the pattern for authentication::
@api.route(before_request=True)
def auth_check(req, resp):
@@ -137,19 +179,36 @@ This is the pattern for authentication guards::
resp.media = {"error": "unauthorized"}
If the ``Authorization`` header is missing, the client gets a 401 response
and the actual route handler never runs.
and the actual route handler never runs. This is cleaner than adding
auth checks to every individual route.
WebSocket hooks work the same way::
@api.before_request(websocket=True)
async def ws_auth(ws):
await ws.accept()
After-Request Hooks
-------------------
The complement to before-request hooks. After-request hooks run after the
route handler completes but before the response is sent. They're useful
for logging, adding response headers, or any post-processing::
@api.after_request()
def log_response(req, resp):
print(f"{req.method} {req.full_url} -> {resp.status_code}")
@api.after_request()
async def add_timing(req, resp):
resp.headers["X-Served-By"] = "responder"
WebSocket Support
-----------------
Responder supports WebSockets for real-time, bidirectional communication::
HTTP is a request-response protocol — the client asks, the server answers.
But some applications need real-time, bidirectional communication: chat
apps, live dashboards, multiplayer games, collaborative editors.
`WebSockets <https://en.wikipedia.org/wiki/WebSocket>`_ solve this by
upgrading an HTTP connection into a persistent, full-duplex channel where
both sides can send messages at any time::
@api.route("/ws", websocket=True)
async def websocket(ws):
@@ -161,14 +220,54 @@ Responder supports WebSockets for real-time, bidirectional communication::
You can send and receive in multiple formats:
- ``send_text`` / ``receive_text`` — plain text
- ``send_json`` / ``receive_json`` — JSON objects
- ``send_text`` / ``receive_text`` — plain text strings
- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized)
- ``send_bytes`` / ``receive_bytes`` — raw binary data
WebSocket routes are marked with ``websocket=True`` in the route decorator.
They receive a ``ws`` object instead of ``req`` and ``resp``.
Server-Sent Events (SSE)
-------------------------
SSE is a simpler alternative to WebSockets for *one-way* real-time
communication — the server pushes events to the client, but the client
can't send messages back. This is perfect for live feeds, progress bars,
notification streams, and AI response streaming.
Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected
by the browser, and doesn't require any special client-side libraries::
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
On the client side, you consume SSE events with JavaScript's built-in
``EventSource`` API::
const source = new EventSource("/events");
source.onmessage = (event) => {
console.log(event.data);
};
Each yielded value can be a string (treated as data) or a dict with the
standard SSE fields::
yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
yield "simple string message"
GraphQL
-------
`GraphQL <https://graphql.org/>`_ is a query language for APIs that lets
clients request exactly the data they need — no more, no less. Instead of
multiple REST endpoints, you define a schema and let clients query it.
Responder includes built-in GraphQL support via
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
with a single method call::
@@ -183,9 +282,10 @@ with a single method call::
api.graphql("/graphql", schema=graphene.Schema(query=Query))
Visiting ``/graphql`` in a browser renders the GraphiQL interactive IDE,
where you can explore your schema and test queries. Programmatic clients
can POST JSON queries to the same endpoint.
Visiting ``/graphql`` in a browser renders the
`GraphiQL <https://github.com/graphql/graphiql>`_ interactive IDE, where
you can explore your schema, write queries, and see results in real-time.
Programmatic clients can POST JSON queries to the same endpoint.
You can access the Responder request and response objects in your resolvers
through ``info.context["request"]`` and ``info.context["response"]``.
@@ -194,8 +294,12 @@ through ``info.context["request"]`` and ``info.context["response"]``.
OpenAPI Documentation
---------------------
Responder can generate an OpenAPI schema and serve interactive API
documentation automatically::
`OpenAPI <https://www.openapis.org/>`_ (formerly Swagger) is the industry
standard for describing REST APIs. An OpenAPI specification lets you
auto-generate interactive documentation, client libraries, and validation
logic.
Responder generates OpenAPI specs from your code::
api = responder.API(
title="Pet Store",
@@ -211,9 +315,11 @@ This gives you:
There are three ways to document your endpoints.
**Pydantic models** — the recommended approach for new APIs. Use
``request_model`` and ``response_model`` to annotate your routes, and
Responder will generate the schema automatically::
**Pydantic models** — the recommended approach. Use ``request_model`` and
``response_model`` to annotate your routes, and Responder generates the
schema automatically. When ``request_model`` is set, request bodies are
also validated automatically — invalid inputs get a ``422`` response with
detailed error messages::
from pydantic import BaseModel
@@ -232,19 +338,11 @@ Responder will generate the schema automatically::
data = await req.media()
resp.media = {"id": 1, **data}
This generates a full OpenAPI path with ``requestBody`` and ``responses``
schemas, all linked by ``$ref`` to your Pydantic models in
``components/schemas``.
When ``response_model`` is set, the response is serialized through the
model — extra fields are stripped and types are enforced.
You can also register standalone schemas with the ``@api.schema`` decorator::
@api.schema("Pet")
class Pet(BaseModel):
name: str
age: int = 0
**YAML docstrings** — inline your OpenAPI spec directly in the docstring.
This gives you full control over every detail::
**YAML docstrings** — for full control, embed OpenAPI YAML in the
docstring::
@api.route("/pets")
def list_pets(req, resp):
@@ -258,8 +356,7 @@ This gives you full control over every detail::
"""
resp.media = [{"name": "Fido"}]
**Marshmallow schemas** — if you're already using marshmallow for
validation, Responder integrates with it via the apispec plugin::
**Marshmallow schemas** — if you're already using marshmallow::
from marshmallow import Schema, fields
@@ -267,19 +364,43 @@ validation, Responder integrates with it via the apispec plugin::
class PetSchema(Schema):
name = fields.Str()
All three approaches can be mixed in the same API. Pydantic models,
marshmallow schemas, and YAML docstrings all contribute to the same
generated OpenAPI specification.
All three approaches can be mixed in the same API. You can choose from
multiple documentation themes: ``swagger_ui`` (default), ``redoc``,
``rapidoc``, or ``elements``.
You can choose from multiple documentation themes:
``swagger_ui`` (default), ``redoc``, ``rapidoc``, or ``elements``.
Route Groups
------------
As your application grows, you'll want to organize routes logically.
Route groups let you share a URL prefix across related endpoints — a
common pattern for API versioning::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
v2 = api.group("/v2")
@v2.route("/users")
def list_users_v2(req, resp):
resp.media = {"users": [], "total": 0}
This keeps your code organized without affecting the routing logic.
Mounting Other Apps
-------------------
Responder can mount any WSGI or ASGI application at a subroute. This means
you can gradually migrate from Flask, or run multiple frameworks side by side::
Responder can mount any WSGI or ASGI application at a subroute. This is
incredibly useful for gradual migrations — you can run Flask and Responder
side by side, moving routes over one at a time::
from flask import Flask
@@ -293,12 +414,17 @@ you can gradually migrate from Flask, or run multiple frameworks side by side::
Requests to ``/flask/`` will be handled by Flask. Everything else goes
through Responder. Both WSGI and ASGI apps are supported — Responder
wraps WSGI apps automatically.
wraps WSGI apps in an ASGI adapter automatically.
Cookies
-------
`Cookies <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>`_ are
small pieces of data that the server asks the browser to store and send
back with every subsequent request. They're the foundation of sessions,
authentication tokens, and user preferences on the web.
Reading and writing cookies is straightforward::
# Read cookies from the request
@@ -307,26 +433,28 @@ Reading and writing cookies is straightforward::
# Set a cookie on the response
resp.cookies["hello"] = "world"
For more control over cookie directives, use ``set_cookie``::
For production use, you'll want to set security directives. The
``httponly`` flag prevents JavaScript from reading the cookie (defending
against XSS attacks), and ``secure`` ensures it's only sent over HTTPS::
resp.set_cookie(
"token",
value="abc123",
max_age=3600,
secure=True,
httponly=True,
max_age=3600, # expires in 1 hour
secure=True, # HTTPS only
httponly=True, # no JavaScript access
path="/",
)
Supported directives: ``key``, ``value``, ``expires``, ``max_age``,
``domain``, ``path``, ``secure``, ``httponly``.
Cookie-Based Sessions
---------------------
Responder has built-in support for signed, cookie-based sessions. Just
read from and write to the ``session`` dictionary::
Sessions let you store per-user data across multiple requests. Responder's
built-in sessions are cookie-based — the session data is serialized, signed
with your secret key, and stored in a cookie. The signature prevents
tampering: if someone modifies the cookie, the signature won't match and
the data will be rejected::
@api.route("/login")
def login(req, resp):
@@ -336,13 +464,9 @@ read from and write to the ``session`` dictionary::
def profile(req, resp):
resp.media = {"user": req.session.get("username")}
The session data is stored in a cookie called ``Responder-Session``. It's
signed for tamper protection, so you can trust that the data originated
from your server.
.. warning::
For production use, always set a secret key::
Always set a secret key in production. The default key is not secret::
api = responder.API(secret_key="your-secret-key-here")
@@ -350,141 +474,98 @@ from your server.
Static Files
------------
Static files are served from the ``static/`` directory by default::
Most web applications serve static assets — CSS stylesheets, JavaScript
files, images, fonts. Responder serves these from the ``static/`` directory
by default::
api = responder.API(static_dir="static", static_route="/static")
Place your CSS, JavaScript, images, and other assets in the ``static/``
directory and they'll be served automatically.
Place your assets in the ``static/`` directory and they'll be served
automatically at ``/static/style.css``, ``/static/app.js``, etc.
For single-page applications, you can serve ``index.html`` as the default
response for all unmatched routes::
For single-page applications (React, Vue, Angular), you can serve
``index.html`` as the default response for all unmatched routes::
api.add_route("/", static=True)
You can add additional static directories at runtime::
api.static_app.add_directory("extra_assets")
CORS
----
Enable Cross-Origin Resource Sharing for your API::
`CORS <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_ (Cross-
Origin Resource Sharing) is a security mechanism that controls which
websites can make requests to your API. Browsers enforce this — if your
API is at ``api.example.com`` and your frontend is at ``app.example.com``,
the browser will block requests unless your API explicitly allows it.
Enable CORS and configure which origins are allowed::
api = responder.API(cors=True, cors_params={
"allow_origins": ["https://example.com"],
"allow_origins": ["https://app.example.com"],
"allow_methods": ["GET", "POST"],
"allow_headers": ["*"],
"allow_credentials": True,
"max_age": 600,
})
The default CORS policy is restrictive — you must explicitly enable the
origins, methods, and headers your frontend needs.
The default policy is restrictive — you must explicitly allow each origin.
Using ``["*"]`` for allow_origins permits any website to call your API,
which is fine for public APIs but not for private ones.
HSTS
----
Force all traffic to HTTPS with a single flag::
`HSTS <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security>`_
(HTTP Strict Transport Security) tells browsers to always use HTTPS when
communicating with your server. Once a browser sees the HSTS header, it
will refuse to connect over plain HTTP, even if the user types ``http://``
in the address bar::
api = responder.API(enable_hsts=True)
This adds the ``Strict-Transport-Security`` header and redirects HTTP
requests to HTTPS.
Trusted Hosts
-------------
Protect against HTTP Host header attacks by restricting which hostnames
your application will respond to::
The ``Host`` header in an HTTP request tells the server which domain name
the client used. Attackers can forge this header to trick your application
into generating URLs to malicious domains (a class of attack called *Host
header injection*).
Restrict which hostnames your application accepts::
api = responder.API(allowed_hosts=["example.com", "*.example.com"])
Requests with a ``Host`` header that doesn't match any of the patterns
will receive a 400 Bad Request response. Wildcard domains are supported.
By default, all hostnames are allowed.
Server-Sent Events (SSE)
------------------------
Stream real-time updates to the client using Server-Sent Events. This is
great for live feeds, progress updates, and AI streaming responses::
@api.route("/events")
async def events(req, resp):
@resp.sse
async def stream():
for i in range(10):
yield {"data": f"message {i}"}
Each yielded value can be a string (treated as data) or a dict with
``data``, ``event``, ``id``, and ``retry`` fields::
yield {"event": "update", "data": "hello", "id": "1"}
yield "simple string message"
Streaming Files
---------------
For large files, use ``resp.stream_file()`` to stream the content without
loading the entire file into memory::
@api.route("/download")
def download(req, resp):
resp.stream_file("large-dataset.csv")
For small files where memory isn't a concern, ``resp.file()`` loads the
entire file at once — simpler but less efficient for large files.
After-Request Hooks
-------------------
Run code after every request, useful for logging, adding headers, or
cleanup::
@api.after_request()
def log_response(req, resp):
print(f"{req.method} {req.full_url} -> {resp.status_code}")
Route Groups
------------
Organize related routes with a shared URL prefix. Useful for API versioning
and logical grouping::
v1 = api.group("/v1")
@v1.route("/users")
def list_users(req, resp):
resp.media = []
@v1.route("/users/{user_id:int}")
def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard
patterns are supported. By default, all hostnames are allowed.
Request ID
----------
Auto-generate unique request IDs for tracing and debugging. If the client
sends an ``X-Request-ID`` header, it's forwarded; otherwise a new UUID is
generated::
In distributed systems, tracing a single request across multiple services
is essential for debugging. Request IDs are unique identifiers attached to
each request — if something goes wrong, you can search your logs for that
ID and find every related event.
Responder can auto-generate request IDs. If the client sends an
``X-Request-ID`` header (common in microservice architectures), it's
forwarded. Otherwise, a new UUID is generated::
api = responder.API(request_id=True)
The ID appears in the ``X-Request-ID`` response header.
Rate Limiting
-------------
Built-in token bucket rate limiter::
Rate limiting prevents individual clients from overwhelming your API with
too many requests. It's essential for public APIs, and good practice even
for internal ones.
Responder includes a built-in token bucket rate limiter::
from responder.ext.ratelimit import RateLimiter
@@ -492,17 +573,25 @@ Built-in token bucket rate limiter::
limiter.install(api)
When the limit is exceeded, clients receive a ``429 Too Many Requests``
response with ``Retry-After`` and ``X-RateLimit-Remaining`` headers.
response with a ``Retry-After`` header. Every response includes
``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients
can pace themselves.
The rate limiter is per-client, keyed by IP address.
MessagePack
-----------
In addition to JSON and YAML, Responder supports MessagePack for efficient
binary serialization::
`MessagePack <https://msgpack.org/>`_ is a binary serialization format
that's more compact and faster to parse than JSON. It's useful for
high-throughput APIs, IoT devices, and anywhere bandwidth matters.
# Decode MessagePack request body
Responder supports MessagePack alongside JSON and YAML::
# Decode a MessagePack request body
data = await req.media("msgpack")
# Content negotiation also works — clients can send
# Accept: application/x-msgpack to receive MessagePack responses.
Content negotiation works too — clients can send
``Accept: application/x-msgpack`` to receive MessagePack responses
instead of JSON.