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