Routing and handlers

MicroPie maps incoming HTTP requests to methods on your App subclass. This section explains how the mapping works and how your handlers receive input from the URL path, query strings, form data and JSON bodies. Session state is accessed explicitly through the request.

URL to function mapping

When an HTTP request arrives, MicroPie extracts the path portion of the URL and uses the first path segment to determine which method should handle the request. For example, a GET request to /greet calls greet on your App subclass. A request to the root URL / calls index. Only methods that do not start with an underscore are exposed as route handlers. Prefacing a method name with an underscore makes it private and hides it from external requests.

Handlers may be synchronous or asynchronous functions. They return either a string, bytes, a JSON‑serialisable object, a tuple of (status_code, body) or (status_code, body, headers), or an iterator/generator for streaming responses. See Streaming responses for details on streaming.

Parameters and arguments

MicroPie automatically populates handler arguments from several sources in the following order:

  1. Path parameters – Additional path segments after the first map to positional parameters. For example:

    class AppExample(App):
        async def greet(self, name):
            return f"Hello, {name}!"
    

    Visiting /greet/Alice passes "Alice" as the name argument. You can also use *args to capture an arbitrary number of path segments.

  2. Query parameters – Named parameters in the query string fill keyword arguments when path parameters are exhausted. For example, /greet?name=Alice also passes "Alice" to the name parameter.

  3. Body data – For POST, PUT and PATCH requests, form fields populate handler arguments as strings. Keys from a top-level JSON object also bind by name, retaining their native JSON types, including nested objects, arrays, numbers, booleans and null.

  4. Files – If the request is multipart/form‑data, uploaded files appear in files. You can declare a file argument in your handler signature to receive a file object.

  5. Default values – If no request source provides a value, default values in your function signature are used.

If MicroPie cannot determine a value for a required parameter, it returns a 400 Bad Request.

Keyword-only parameters are passed by keyword, and **kwargs collects remaining named query, form, JSON and file values after explicit parameters have been bound. *args continues to collect remaining path segments.

Session values never participate in automatic argument binding. This keeps untrusted request inputs and trusted session state in separate namespaces. Read session values explicitly inside the handler:

class MyApp(App):
    async def transfer(self, amount):
        user_id = self.request.session["user_id"]
        return await make_transfer(user_id, amount)

An extra user_id query or body field cannot replace the value read from the session in this example.

Choosing an input access style

MicroPie gives you three levels of request input access. Prefer the simplest level that fits the handler.

  1. Handler arguments are the most concise option. Use them when an endpoint needs a small number of named values and the normal binding order is acceptable.

    class MyApp(App):
        async def search(self, q="", page="1"):
            return {"q": q, "page": int(page)}
    

    This works for /search?q=micropie&page=2 without touching self.request directly.

  2. Helper methods are best when the handler needs explicit control over where a value comes from, a default value, or the full JSON payload.

    class MyApp(App):
        async def search(self):
            q = self.request.query("q", "")
            page = int(self.request.query("page", "1"))
            return {"q": q, "page": page}
    
        async def submit(self):
            username = self.request.form("username", "Anonymous")
            return {"submitted_by": username}
    
        async def api_submit(self):
            payload = self.request.json()
            username = self.request.json("username", "Anonymous")
            return {"submitted_by": username, "raw": payload}
    

    The helper methods are:

    • self.request.query(name, default=None): first query-string value for name.

    • self.request.form(name, default=None): first parsed body value for name from URL-encoded or multipart form data.

    • self.request.json(name=None, default=None): full parsed JSON payload when called with no name, or one top-level JSON object value when name is provided.

  3. Raw attributes are useful when you need the lower-level parsed structures, such as repeated query parameters, all submitted form values, uploaded files, or middleware decisions.

    • self.request.path_params: list of positional path segments.

    • self.request.query_params: dict[str, list[str]] parsed from the query string.

    • self.request.body_params: dict[str, list[str]] parsed from form-urlencoded payloads or multipart text fields. JSON data is available only through json() and native JSON argument binding.

    • self.request.session: session dictionary.

    • self.request.files: uploaded multipart files.

    • self.request.headers: lower-case request header mapping.

    For example, use query_params when repeated values matter:

    class MyApp(App):
        async def filter(self):
            tags = self.request.query_params.get("tag", [])
            return {"tags": tags}
    

Query strings and URL-encoded form bodies share MicroPie’s permissive parsing behavior for compatibility. Blank values such as name= and bare keys are omitted, so they fall through to handler defaults. Malformed percent escapes are tolerated, invalid percent-decoded UTF-8 is replaced, and invalid raw UTF-8 bytes are ignored. HTTP and WebSocket query strings use the same rules.

Do not await request helpers

The request helper methods are synchronous accessors over data MicroPie has already parsed for the current request. Do not use await with query(), form() or json():

class MyApp(App):
    async def submit(self):
        data = self.request.json()
        username = self.request.form("username", "")
        q = self.request.query("q", "")
        return {"data": data, "username": username, "q": q}

For JSON and URL-encoded form requests, the body has been read before your handler runs. Multipart file uploads are the main exception: uploaded file content is exposed as an asyncio.Queue subclass in self.request.files and should be read asynchronously. File producers do not block discovery of later multipart parts; the application’s total body limit bounds queued content. A queue yields None only for a clean end-of-file. If parsing aborts, get() raises MultipartFileError so a partial file cannot be mistaken for a complete upload.

Examples

The following examples illustrate common patterns:

class MyApp(App):
    async def greet(self, name="Guest"):
        """Return a greeting for a named visitor.

        If the ``name`` argument is not provided via the path or
        query parameters, ``"Guest"`` is used as a default.
        """
        return f"Hello, {name}!"

    async def add(self, x, y):
        """Add two numbers provided via path segments.

        Example: ``/add/2/3`` returns ``5``.
        """
        return str(int(x) + int(y))

    async def submit(self, username="Anonymous"):
        """Handle a form submission.

        This handler accepts POST requests and uses the
        ``username`` field from the request body.
        """
        return f"Form submitted by: {username}"

See Request objects for the attributes available on the current request and App class for details about the App class.

HTTP methods and responses

Handlers run for any HTTP method unless you explicitly check self.request.method inside the handler. It is your responsibility to dispatch based on the method if your endpoint should behave differently for GET and POST. Handlers may return:

  • A string or bytes – sent as the body of the response with a 200 OK status.

  • A JSON‑serialisable object – automatically encoded into JSON and returned with a Content‑Type of application/json.

  • A tuple (status_code, body) – sets the HTTP status and body.

  • A tuple (status_code, body, headers) – sets status, body and additional headers. Headers should be a list of (name, value) pairs.

  • A generator or async generator – streams chunks to the client. Use this for large responses or server‑sent events.

Advanced routing

You can implement explicit routing, path prefixing or complex dispatching by writing a custom HttpMiddleware. See the Writing middleware guide and the examples in the examples/explicit_routing directory of the source distribution.