Response

Route handlers should return a response that will be sent back to the client. Expanse supports sending various types of responses: HTML fragments, JSON objects, files, and more.

Sending responses

Native Python types

In simple cases, you can return native objects that will be automatically converted to a response:

def hello_world() -> str:
    return "Hello world!"


def json_object() -> dict[str, Any]:
    return {"foo": "bar"}


router.get("/hello", hello_world)
router.get("/json", json_object)

In these cases Expanse will automatically convert the returned value to a JSON (application/json) response.

Response objects

Most of the time you will want to return more complex responses than simple strings or dictionaries. Instead, you will return Response instances or views, which will give you more control over the status code, headers or cookies of the response.

from expanse.http.response import Response


def hello_world() -> Response:
    response = Response("Hello world!", 200)
    response.headers["Content-Type"] = "application/json"

    return response

Adding headers

You can set headers on the response object to control the response behavior by using the with_header() method:

from expanse.http.response import Response


def hello_world() -> Response:
    return (
        Response("Hello world!", 200)
        .with_header("X-Header-One", "Value")
        .with_header("X-Header-Two", "Value")
    )

You can also use with_headers() to specify multiple headers to be added to the response:

from expanse.http.helpers import json
from expanse.http.response import Response


def hello_world() -> Response:
    return json("Hello world!", 200).with_headers(
        {"X-Header-One": "Value", "X-Header-Two": "Value"}
    )

Adding cookies

You can set cookies on the response object by using the with_cookie() method:

from expanse.http.response import Response


def hello_world() -> Response:
    return Response("Hello world!", 200).with_cookie("name", "value", 3600)

You must give the cookie a name, a value and an expiration time in seconds. The expiration time represents the time the cookie will be considered valid.

Encrypted cookies

By default, all cookies are encrypted, thanks to the EncryptCookies middleware, so that they can't be neither read nor tampered with by the client. If you would like to disable encryption for specific cookies, you can do so when configuring the middleware:

from expanse.http.middleware.encrypt_cookies import EncryptCookies
from expanse.core.http.middleware.middleware_stack import MiddlewareStack


async def configure_middleware(middleware: MiddlewareStack):
    middleware.group("web").replace(
        EncryptCookies,
        EncryptCookies.excluding("cookie_name", "other_cookie")
    )

Other response types

JSON

You can use the json() helper function to create a JSON response easily:

from expanse.http.helpers import json
from expanse.http.response import Response


def hello_world() -> Response:
    return json("Hello world!", 200)

HTML

You can use the html() helper function to create an HTML response easily:

from expanse.http.helpers import html
from expanse.http.response import Response


def hello_world() -> Response:
    return html("<h1>Hello world!</h1>", 200)

Text

You can use the text() helper function to create a plain text response:

from expanse.http.helpers import text

from expanse.http.response import Response


def hello_world() -> Response:
    return text("Hello world!", 200)

File responses

If you want to display a file in the browser — for instance, an image or a PDF document — you can use the file_() helper function.

from expanse.http.helpers import file_
from expanse.http.response import Response


def display_file() -> Response:
    return file_("path/to/file.txt")

Downloading files

If, instead, you want to download the file, you can use the download() helper function:

from expanse.http.helpers import download
from expanse.http.response import Response


def download_file() -> Response:
    return download("path/to/file.txt")

By default, the filename that will be visible to the user will be the same as the given file's name. If you want to change the filename, you can pass it via the filename parameter:

from expanse.http.helpers import download
from expanse.http.response import Response


def download_file() -> Response:
    return download("path/to/file.txt", filename="new_name.txt")

Redirection

If you need to redirect the user to a specific URL, you retrieve a Redirect instance from the Responder by using the redirect() method:

from expanse.http.helpers import redirect
from expanse.http.response import Response


def profile() -> Response:
    return redirect().to("/me/profile")

Redirecting to named routes

If you want to redirect to a named route, you can use the to_route() method of the Redirect instance:

from expanse.http.helpers import redirect
from expanse.http.response import Response


def profile() -> Response:
    return redirect().to_route("user.profile")

If the named route requires parameters, you can specify them as a dictionary:

redirect().to_route("user.profile", {"id": 1})

Any additional parameters will be added to the query string of the generated URL:

redirect().to_route("users.retrieve", {"id": 1, "extended": "true"})

Responding with models

In some cases, you will manage models in your application and want to return them as JSON responses. Fortunately, Expanse supports models as responses out of the box.

from sqlalchemy.orm import Session


def get_user(session: Session, user_id: int) -> User:
    return session.get(User, user_id)

Note that this only works for models that are composed of columns of only primitive types (int, str, float, etc.) that can be serialized to JSON. If your model contains relationships or other complex types, you will need to provide a serialization model.

Serializing models

To convert complex models to JSON responses — or if you want to control the serialization behavior — you can use a serialization model.

from typing import Annotated
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.models.user import User


class UserData(BaseModel):
    id: int
    email: str


def get_user(
    session: Session, user_id: int
) -> Annotated[User, UserData]:
    return session.get(User, user_id)

When called the endpoint will return a JSON response with the serialized data:

{
  "id": 1,
  "email": "[email protected]"
}

This works the same way if you want to return a list of models:

from typing import Annotated
from typing import Sequence

from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.models.user import User


class UserData(BaseModel):
    id: int
    email: str


def list_users(session: Session) -> Sequence[Annotated[User, UserData]]:
    return session.scalars(User).all()

This endpoint will return a JSON response with an array of serialized data:

[
  {
    "id": 1,
    "email": "[email protected]"
  },
  {
    "id": 2,
    "email": "[email protected]"
  }
]

Deferred actions

If you want to perform some actions after the response has been sent to the client, you can use the defer() method of response objects.

import asyncio

from expanse.http.response import Response


async def long_running_task():
    # Simulate a long-running task
    await asyncio.sleep(5)
    print("Long-running task completed!")


def hello_world() -> Response:
    response = Response("Hello world!", 200)
    response.defer(long_running_task)

    return response