diff --git a/docs/source/deployment.rst b/docs/source/deployment.rst
index f30b59c..7644ceb 100644
--- a/docs/source/deployment.rst
+++ b/docs/source/deployment.rst
@@ -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 `_
+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 `_ server on
+``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on
+`uvloop `_ — 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 `_ 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 `_ 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 `_ or `Caddy `_ 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.
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
index f675fc3..1781eed 100644
--- a/docs/source/quickstart.rst
+++ b/docs/source/quickstart.rst
@@ -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 `_
+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 = "
HTML response
"
-**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 `_
-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 `_, 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::
+
+ Hello, {{ name }}!
+
+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.
diff --git a/docs/source/testing.rst b/docs/source/testing.rst
index 0e926af..6271d2d 100644
--- a/docs/source/testing.rst
+++ b/docs/source/testing.rst
@@ -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,
diff --git a/docs/source/tour.rst b/docs/source/tour.rst
index deef200..962c720 100644
--- a/docs/source/tour.rst
+++ b/docs/source/tour.rst
@@ -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 `_.
+
+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 `_ 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 `_ 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 `_. 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 `_ 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 `_ (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 `_ 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 `_ (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 `_
+(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 `_ 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.