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.