mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 06:46:14 +00:00
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:
+50
-33
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user