Add Pydantic support for OpenAPI schema generation

Define your API schemas with Pydantic models instead of (or alongside)
YAML docstrings and marshmallow:

    from pydantic import BaseModel

    class PetIn(BaseModel):
        name: str
        age: int = 0

    class PetOut(BaseModel):
        id: int
        name: str
        age: int

    @api.route("/pets", methods=["POST"],
               request_model=PetIn, response_model=PetOut)
    async def create_pet(req, resp):
        data = await req.media()
        resp.media = {"id": 1, **data}

Also works with @api.schema("Name") decorator for registering
standalone schema components.

Pydantic models, marshmallow schemas, and YAML docstrings can all
be used together in the same API.

Also: rewrite docs with more prose, restore sidebar logo and links,
add FastAPI acknowledgment, update homepage copy.

161 tests, 95% coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 12:35:07 -04:00
parent 33ebc77f10
commit 1bfd85b003
11 changed files with 724 additions and 142 deletions
+9 -3
View File
@@ -1,8 +1,14 @@
<p class="logo">
<a href="{{ pathto(master_doc) }}">
<img class="logo" src="{{ pathto('_static/responder.png', 1) }}" />
</a>
</p>
<p>
<strong>Responder</strong> — a familiar HTTP service framework for Python.
</p>
<h3>Useful Links</h3>
<ul>
<li><a href="https://github.com/kennethreitz/responder">GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">PyPI</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issues</a></li>
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
</ul>
+2 -1
View File
@@ -39,7 +39,8 @@ html_theme_options = {
}
html_static_path = ["_static"]
html_sidebars = {
"**": ["localtoc.html", "searchbox.html"],
"index": ["sidebarintro.html", "searchbox.html"],
"**": ["sidebarintro.html", "localtoc.html", "searchbox.html"],
}
# MyST
+61 -6
View File
@@ -1,10 +1,34 @@
Deployment
==========
Responder applications are standard ASGI apps. You can deploy them anywhere
you'd deploy a Python web service.
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!"
if __name__ == "__main__":
api.run()
This starts a production uvicorn server on ``127.0.0.1:5042``.
Docker
------
::
A minimal Dockerfile for deploying a Responder application::
FROM python:3.13-slim
WORKDIR /app
@@ -14,18 +38,49 @@ Docker
EXPOSE 80
CMD ["python", "api.py"]
Build and run::
$ docker build -t myapi .
$ docker run -p 8000:80 myapi
Cloud Platforms
---------------
Responder honors the ``PORT`` environment variable automatically.
It works with any platform that sets ``PORT``: Fly.io, Railway, Render,
Google Cloud Run, etc.
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.
This works out of the box with:
- **Fly.io**
- **Railway**
- **Render**
- **Google Cloud Run**
- **Azure Container Apps**
- **AWS App Runner**
Just deploy your code and set the start command to ``python api.py``.
Uvicorn Directly
----------------
For more control, run with uvicorn::
For more control over the production server, you can bypass ``api.run()``
and use uvicorn directly::
uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
$ 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.
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.
Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work
correctly behind proxies that set standard forwarding headers.
+71 -4
View File
@@ -1,7 +1,7 @@
Responder
=========
A familiar HTTP Service Framework for Python, powered by `Starlette`_.
A familiar HTTP Service Framework for Python.
.. code:: python
@@ -16,14 +16,76 @@ A familiar HTTP Service Framework for Python, powered by `Starlette`_.
if __name__ == '__main__':
api.run()
Install it::
Powered by `Starlette`_ and `uvicorn`_. The ``async`` is optional.
pip install responder
Python 3.9+.
The Idea
--------
Responder takes the best ideas from `Flask`_ and `Falcon`_ and brings them
together into one clean framework.
The request and response objects are passed into every view and mutated
directly — no return values, no boilerplate. If you've used Requests,
you'll feel right at home. If you've used Flask, the routing will look
familiar. If you've used Falcon, the ``req`` / ``resp`` pattern will
click immediately.
- ``resp.text`` sends back text. ``resp.html`` sends back HTML.
- ``resp.media`` sends back JSON — or YAML, if the client asks for it.
- ``resp.file("path")`` serves a file. ``resp.content`` sends raw bytes.
- ``req.headers`` is case-insensitive. ``req.params`` holds query parameters.
- ``resp.status_code``, ``req.method``, ``req.url`` — the usual suspects.
Content negotiation happens automatically. Set ``resp.media`` to a dict
and Responder figures out the rest.
Responder and `FastAPI`_ share DNA — both are built on Starlette, both
appeared around the same time, and both pushed Python's ASGI ecosystem
forward. FastAPI went deep on type annotations and automatic validation.
Responder went for a mutable request/response pattern and a simpler,
more familiar API. Both projects are better for the other existing, and
you should use whichever feels right for what you're building.
What You Get
------------
One ``pip install``, batteries included:
- Mount Flask, Django, or any WSGI/ASGI app at a subroute.
- Gzip compression, HSTS, CORS, and trusted host validation.
- Before-request hooks that can short-circuit for auth guards.
- A test client for fast, in-process testing with pytest.
- Route parameters with f-string syntax and type convertors.
- Lifespan context managers for startup and shutdown logic.
- Custom exception handlers for clean error responses.
- `GraphQL`_ with Graphene and a built-in GraphiQL IDE.
- File serving with automatic content-type detection.
- Sync and async views — ``async`` is always optional.
- Class-based views with ``on_get``, ``on_post``, ``on_request``.
- A pleasant API with a single import statement.
- OpenAPI schema generation with Swagger UI.
- A production `uvicorn`_ server, ready to deploy.
- HTTP method filtering for REST APIs.
- Signed cookie-based sessions.
- Background tasks in a thread pool.
- WebSocket support.
Installation
------------
.. code-block:: shell
$ uv pip install responder
Python 3.9 and above. That's it.
.. toctree::
:maxdepth: 2
:caption: User Guide
quickstart
tour
@@ -42,3 +104,8 @@ Python 3.9+.
.. _Starlette: https://www.starlette.io/
.. _uvicorn: https://www.uvicorn.org/
.. _Flask: https://flask.palletsprojects.com/
.. _Falcon: https://falconframework.org/
.. _FastAPI: https://fastapi.tiangolo.com/
.. _GraphQL: https://graphql.org/
+151 -60
View File
@@ -1,143 +1,234 @@
Quick Start
===========
Create an API
-------------
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.
::
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::
import responder
api = responder.API()
Add a route
Hello World
-----------
::
Next, add a route. Here, we'll make the root URL say "hello, world!"::
@api.route("/")
def hello(req, resp):
def hello_world(req, resp):
resp.text = "hello, world!"
Run it
------
Every view receives a ``req`` (request) and ``resp`` (response) object. You
don't need to return anything — just mutate the response directly.
::
Run the Server
--------------
Start your web service with ``api.run()``::
api.run()
This starts a production uvicorn server on port ``5042``. Customize with
``api.run(port=8000)`` or set the ``PORT`` environment variable.
This spins up a production-grade uvicorn server on port ``5042``, ready for
incoming HTTP requests.
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.
.. note::
Both sync and async views are supported. The ``async`` keyword is always
optional — use it when you need to ``await`` something.
Route Parameters
----------------
Use f-string syntax for dynamic URLs::
If you want dynamic URLs, use Python's familiar f-string syntax to declare
variables in your routes::
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
Type convertors are available::
A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``.
Route parameters are passed as keyword-only arguments (after the ``*``).
Type Convertors
^^^^^^^^^^^^^^^
You can constrain route parameters to specific types. The parameter will be
automatically converted before it reaches your view::
@api.route("/add/{a:int}/{b:int}")
async def add(req, resp, *, a, b):
resp.text = f"{a} + {b} = {a + b}"
Supported types: ``str``, ``int``, ``float``, ``uuid``, ``path``.
Supported types:
- ``str`` — matches any string without slashes (default)
- ``int`` — matches digits, converts to ``int``
- ``float`` — matches decimal numbers, converts to ``float``
- ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000``
- ``path`` — matches any string *including* slashes, useful for file paths
Responses
---------
Sending Responses
-----------------
::
Responder gives you several ways to send data back to the client. Just set
the appropriate property on the response object.
# Text
resp.text = "hello"
**Text and HTML**::
# HTML
resp.html = "<h1>hello</h1>"
resp.text = "plain text response"
resp.html = "<h1>HTML response</h1>"
# JSON (default)
resp.media = {"hello": "world"}
**JSON** — the most common pattern for APIs. Set ``resp.media`` to any
JSON-serializable Python object::
# Bytes
resp.content = b"\x00\x01\x02"
@api.route("/hello/{who}/json")
def hello_json(req, resp, *, who):
resp.media = {"hello": who}
# File
resp.file("report.pdf")
If the client sends an ``Accept: application/x-yaml`` header, the same data
will be returned as YAML instead. Content negotiation is automatic.
**Files** — serve a file from disk with automatic content-type detection::
resp.file("reports/annual.pdf")
**Raw bytes**::
resp.content = b"\x89PNG\r\n..."
**Status codes and headers**::
# Status code
resp.status_code = 201
# Headers
resp.headers["X-Custom"] = "value"
# Redirect
api.redirect(resp, location="/other")
**Redirects**::
api.redirect(resp, location="/new-url")
Requests
--------
Reading Requests
----------------
::
The request object gives you access to everything the client sent.
# Method (lowercase)
req.method # "get", "post", etc.
**Method and URL**::
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 (case-insensitive)
req.headers["Content-Type"]
req.headers["content-type"] # same thing
# Query parameters
req.params["q"]
**Query parameters**::
# Path parameters
req.path_params["user_id"]
# GET /search?q=python&page=2
req.params["q"] # "python"
req.params["page"] # "2"
# JSON body (must await)
**Path parameters** — also available on the request object::
req.path_params["user_id"] # same as the keyword argument
**Request body** — for POST/PUT/PATCH requests, you need to ``await`` the
body content::
# JSON body
data = await req.media()
# Raw body
# Form data
data = await req.media("form")
# File uploads
files = await req.media("files")
# Raw bytes
body = await req.content
# Check content type
req.is_json # True/False
# Raw text
text = await req.text
# Client address
req.client # (host, port)
**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
Templates
---------
Rendering Templates
-------------------
Responder includes Jinja2 templating::
Responder includes built-in `Jinja2 <https://jinja.palletsprojects.com/>`_
support. Templates are loaded from the ``templates/`` directory by default.
The simplest way is to use ``api.template()``::
@api.route("/hello/{name}/html")
def hello_html(req, resp, *, name):
resp.html = api.template("hello.html", name=name)
Or use the ``Templates`` class directly::
You can also use the ``Templates`` class directly for more control::
from responder.templates import Templates
templates = Templates(directory="templates")
resp.html = templates.render("page.html", title="Hello")
@api.route("/page")
def page(req, resp):
resp.html = templates.render("page.html", title="Hello")
Async rendering is supported too::
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::
resp.html = api.template_string("Hello, {{ name }}!", name="world")
Background Tasks
----------------
Process work in the background while responding immediately::
Sometimes you want to accept a request, respond immediately, and do the
actual processing later. Responder makes this easy with background tasks::
@api.route("/work")
async def work(req, resp):
@api.route("/incoming")
async def receive_incoming(req, resp):
data = await req.media()
@api.background.task
def process(data):
def process_data(data):
"""This runs in a background thread."""
import time
time.sleep(10)
time.sleep(10) # simulate heavy work
process(data)
resp.media = {"status": "processing"}
process_data(data)
# Respond immediately — processing continues 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.
+84 -19
View File
@@ -1,12 +1,15 @@
Testing
=======
Responder includes a built-in test client powered by Starlette's TestClient.
Responder includes a built-in test client powered by Starlette's
``TestClient``. You don't need to start a server — tests run in-process,
making them fast and reliable.
Basic Test
----------
``api.py``::
Getting Started
---------------
Given a simple application in ``api.py``::
import responder
@@ -19,15 +22,16 @@ Basic Test
if __name__ == "__main__":
api.run()
``test_api.py``::
You can test it with pytest::
# test_api.py
import api as service
def test_hello():
r = service.api.requests.get("/")
assert r.text == "hello, world!"
Run with pytest::
Run your tests::
$ pytest
@@ -35,7 +39,8 @@ Run with pytest::
Using Fixtures
--------------
::
For larger test suites, use pytest fixtures to share the API instance
across tests::
import pytest
import api as service
@@ -56,11 +61,47 @@ Using Fixtures
r = api.requests.get(api.url_for(data))
assert r.json() == {"key": "value"}
The ``api.url_for()`` method generates a URL for a given route endpoint,
so you don't have to hard-code paths in your tests.
Testing JSON APIs
-----------------
Send JSON data and check the response::
def test_create_item(api):
@api.route("/items")
async def create(req, resp):
data = await req.media()
resp.media = {"created": data}
resp.status_code = 201
r = api.requests.post(api.url_for(create), json={"name": "widget"})
assert r.status_code == 201
assert r.json() == {"created": {"name": "widget"}}
Testing File Uploads
--------------------
Send files using the ``files`` parameter::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"received": list(files.keys())}
files = {"doc": ("report.pdf", b"content", "application/pdf")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"received": ["doc"]}
Testing WebSockets
------------------
::
Use Starlette's ``TestClient`` directly for WebSocket connections::
from starlette.testclient import TestClient
@@ -76,17 +117,41 @@ Testing WebSockets
assert ws.receive_text() == "hello"
Testing File Uploads
--------------------
Testing Error Handling
----------------------
::
To test error responses without pytest raising the exception, disable
server exception propagation::
def test_upload(api):
@api.route("/upload")
async def upload(req, resp):
files = await req.media("files")
resp.media = {"name": list(files.keys())[0]}
from starlette.testclient import TestClient
files = {"doc": ("test.txt", b"content", "text/plain")}
r = api.requests.post(api.url_for(upload), files=files)
assert r.json() == {"name": "doc"}
def test_500(api):
@api.route("/fail")
def fail(req, resp):
raise ValueError("something broke")
client = TestClient(api, raise_server_exceptions=False)
r = client.get(api.url_for(fail))
assert r.status_code == 500
Testing Lifespan Events
-----------------------
The test client supports lifespan events. Use ``with`` to ensure startup
and shutdown hooks run::
def test_with_lifespan(api):
started = {"value": False}
@api.on_event("startup")
async def on_startup():
started["value"] = True
@api.route("/")
def check(req, resp):
resp.media = {"started": started["value"]}
with api.requests as session:
r = session.get("http://;/")
assert r.json() == {"started": True}
+162 -45
View File
@@ -1,11 +1,15 @@
Feature Tour
============
This section walks through Responder's features in detail. Each section
includes working code examples you can copy into your application.
Method Filtering
----------------
Restrict routes to specific HTTP methods::
By default, a route matches all HTTP methods. If you want to restrict a
route to specific methods, pass the ``methods`` parameter::
@api.route("/items", methods=["GET"])
def list_items(req, resp):
@@ -16,11 +20,15 @@ Restrict routes to specific HTTP methods::
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.
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::
@api.route("/{greeting}")
class GreetingResource:
@@ -31,28 +39,36 @@ Class-Based Views
resp.media = {"received": greeting}
def on_request(self, req, resp, *, greeting):
"""Called on every request method."""
"""Called on EVERY request, before the method-specific handler."""
resp.headers["X-Greeting"] = greeting
The ``on_request`` method is called for all HTTP methods, much like
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.
Lifespan Events
---------------
Use a context manager for startup and shutdown::
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::
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
# Startup
# Startup — runs before the first request
print("connecting to database...")
yield
# Shutdown
# Shutdown — runs after the server stops
print("closing connections...")
api = responder.API(lifespan=lifespan)
Or use event decorators::
You can also use the traditional event decorator style::
@api.on_event("startup")
async def startup():
@@ -62,42 +78,57 @@ Or use event decorators::
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.
File Serving
------------
Serve files with automatic content-type detection::
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::
@api.route("/download")
def download(req, resp):
resp.file("reports/annual.pdf")
You can override the content type if needed::
@api.route("/image")
def image(req, resp):
resp.file("photos/cat.jpg", content_type="image/jpeg")
Error Handling
--------------
Custom Error Handling
---------------------
Register handlers for specific exception types::
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::
@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.
Before-Request Hooks
--------------------
Run code before every request::
Run code before every request. This is useful for logging, adding common
headers, or setting up per-request state::
@api.route(before_request=True)
def add_headers(req, resp):
resp.headers["X-API-Version"] = "3.1"
Short-circuit by setting a status code — the route handler will be skipped::
**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::
@api.route(before_request=True)
def auth_check(req, resp):
@@ -105,11 +136,20 @@ Short-circuit by setting a status code — the route handler will be skipped::
resp.status_code = 401
resp.media = {"error": "unauthorized"}
If the ``Authorization`` header is missing, the client gets a 401 response
and the actual route handler never runs.
WebSockets
----------
WebSocket hooks work the same way::
::
@api.before_request(websocket=True)
async def ws_auth(ws):
await ws.accept()
WebSocket Support
-----------------
Responder supports WebSockets for real-time, bidirectional communication::
@api.route("/ws", websocket=True)
async def websocket(ws):
@@ -119,46 +159,79 @@ WebSockets
await ws.send_text(f"Hello {name}!")
await ws.close()
Supported formats: ``send_text``, ``send_json``, ``send_bytes``.
You can send and receive in multiple formats:
- ``send_text`` / ``receive_text`` — plain text
- ``send_json`` / ``receive_json`` — JSON objects
- ``send_bytes`` / ``receive_bytes`` — raw binary data
GraphQL
-------
One-liner setup with `Graphene <https://graphene-python.org/>`_::
Responder includes built-in GraphQL support via
`Graphene <https://graphene-python.org/>`_. Set up a full GraphQL endpoint
with a single method call::
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
def resolve_hello(self, info, name):
return f"Hello {name}"
api.graphql("/graphql", schema=graphene.Schema(query=Query))
Visiting ``/graphql`` in a browser renders the GraphiQL IDE.
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.
You can access the Responder request and response objects in your resolvers
through ``info.context["request"]`` and ``info.context["response"]``.
OpenAPI
-------
OpenAPI Documentation
---------------------
::
Responder can generate an OpenAPI schema and serve interactive API
documentation automatically::
api = responder.API(
title="My API",
title="Pet Store",
version="1.0",
openapi="3.0.2",
docs_route="/docs",
)
Visit ``/docs`` for interactive Swagger UI documentation.
The schema is served at ``/schema.yml``.
This gives you:
- An OpenAPI schema at ``/schema.yml``
- Interactive Swagger UI documentation at ``/docs``
Document your endpoints using YAML in docstrings::
@api.route("/pets")
def list_pets(req, resp):
"""A list of pets.
---
get:
description: Get all pets
responses:
200:
description: A list of pets
"""
resp.media = [{"name": "Fido"}]
You can choose from multiple documentation themes:
``swagger_ui`` (default), ``redoc``, ``rapidoc``, or ``elements``.
Mounting Apps
-------------
Mounting Other Apps
-------------------
Mount any WSGI or ASGI application at a subroute::
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::
from flask import Flask
@@ -170,26 +243,42 @@ Mount any WSGI or ASGI application at a subroute::
api.mount("/flask", flask_app)
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.
Cookies
-------
::
Reading and writing cookies is straightforward::
# Read cookies
req.cookies["session_id"]
# Read cookies from the request
session_id = req.cookies.get("session_id")
# Set cookies
# Set a cookie on the response
resp.cookies["hello"] = "world"
# With directives
resp.set_cookie("token", value="abc", max_age=3600, secure=True)
For more control over cookie directives, use ``set_cookie``::
resp.set_cookie(
"token",
value="abc123",
max_age=3600,
secure=True,
httponly=True,
path="/",
)
Supported directives: ``key``, ``value``, ``expires``, ``max_age``,
``domain``, ``path``, ``secure``, ``httponly``.
Sessions
--------
Cookie-Based Sessions
---------------------
Built-in cookie-based sessions::
Responder has built-in support for signed, cookie-based sessions. Just
read from and write to the ``session`` dictionary::
@api.route("/login")
def login(req, resp):
@@ -199,9 +288,15 @@ Built-in cookie-based sessions::
def profile(req, resp):
resp.media = {"user": req.session.get("username")}
Set a secret key for production::
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.
api = responder.API(secret_key="your-secret-key")
.. warning::
For production use, always set a secret key::
api = responder.API(secret_key="your-secret-key-here")
Static Files
@@ -211,34 +306,56 @@ Static files are served from the ``static/`` directory by default::
api = responder.API(static_dir="static", static_route="/static")
For single-page apps, serve ``index.html`` as the default::
Place your CSS, JavaScript, images, and other assets in the ``static/``
directory and they'll be served automatically.
For single-page applications, 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::
api = responder.API(cors=True, cors_params={
"allow_origins": ["https://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.
HSTS
----
Redirect all traffic to HTTPS::
Force all traffic to HTTPS with a single flag::
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::
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.