# FastAPI
`chameleon_rs.fastapi` renders templates from FastAPI views. It has fastapi-chameleon's API, so moving an app over is an import change, or no change at all with [chameleon_rs.compat.install()](../reference/compat.install.md#chameleon_rs.compat.install) (which makes `import fastapi_chameleon` give this module).
`import chameleon_rs` doesn't import FastAPI. Only `import chameleon_rs.fastapi` does. There is nothing extra to install: it uses the FastAPI your app already has.
# A first view
``` python
import fastapi
from fastapi.testclient import TestClient
import chameleon_rs.fastapi as fastapi_chameleon
app = fastapi.FastAPI()
fastapi_chameleon.configure(template_folder=template_folder, auto_reload=dev_mode, app=app)
@app.get('/')
@fastapi_chameleon.template('home/index.pt')
async def index(limit: int = 2):
videos = ['intro', 'partials', 'async']
return {'message': 'Hello', 'videos': videos[:limit]}
```
The view returns the template's model, and `@template` renders it into a `fastapi.Response` (`text/html`):
``` python
with TestClient(app) as client:
print(client.get('/?limit=1').text)
print(client.get('/?limit=lots').status_code) # FastAPI's own validation
```
Hello
422
The decorated view keeps its signature, so path and query parameters, `Depends`, validation errors and the OpenAPI docs are exactly what they would be without `@template`.
[configure()](../reference/configure.md#chameleon_rs.configure) here is the same call as [chameleon_rs.configure()](../reference/configure.md#chameleon_rs.configure), so it also tells [render_partial](../reference/render_partial.md#chameleon_rs.render_partial) where templates live. `app=` turns on the decorator-order check below.
# Decorator order
`@template` goes below the route decorator. Written above it, FastAPI registers the bare view, the template never renders and the route quietly returns the view's dict as JSON. An app given to `configure(..., app=app)` checks this when it starts:
``` python
bad = fastapi.FastAPI()
fastapi_chameleon.configure(app=bad)
router = fastapi.APIRouter()
@fastapi_chameleon.template('home/index.pt') # wrong: above the route
@router.get('/about')
def about():
return {'message': 'About', 'videos': []}
bad.include_router(router)
try:
with TestClient(bad):
pass
except Exception as error:
print(type(error).__name__)
print(str(error).split(' at ')[0])
```
DecoratorOrderError
@template must be placed below the route decorator (@app.get, @router.get, @app.api_route, ...) on about()
The check runs inside the app's lifespan startup, so Uvicorn and Granian report a failed startup and exit. When no lifespan runs (a server with lifespan events off, a test client used without `with`, a mounted sub-application) it runs before the first request instead. Routers included with `include_router` and mounted apps are checked too. A function registered raw on purpose (say, JSON on one route) and also decorated on another route is not flagged. `fastapi_chameleon.check_views(app)` runs the same check on demand.
One pattern can't be told from the mistake: a function routed raw on this app whose `@template` wrapper is used somewhere else (called from another view, or routed on another app). For such an app, pass `check_order=False` to [configure()](../reference/configure.md#chameleon_rs.configure) and keep the check in a test with `fastapi_chameleon.check_views(app, ignore=[that_function])`.
# Sync and async views
- An `async def` view renders its page with [render_async](../reference/robyn.render_async.md#chameleon_rs.robyn.render_async) on the event loop. A model value that is awaitable (a coroutine, task or future) is awaited where the template uses it, so a slow query can be handed to the template unawaited.
- A plain `def` view renders with [render()](../reference/flask.render.md#chameleon_rs.flask.render). FastAPI runs the view, render included, in its thread pool, as with fastapi-chameleon.
`@template(..., concurrent=True)` renders `render_partial(...)` calls with awaitable arguments concurrently; `@template(..., offload=True)` (experimental) renders in a worker thread.
The template gets what the view returns, and nothing else: unlike `chameleon_rs.starlette`'s endpoints, a FastAPI view's template does not get `request` or `url_for` added, as with fastapi-chameleon (a FastAPI view has no request unless it declares one). Return them in the model if the template needs them.
# What a view can return
- **A model:** a `dict`, any other mapping, a dataclass instance (its fields, shallowly) or a Pydantic model (anything with a callable `model_dump()`).
- **A response:** any Starlette response (`fastapi.Response`, `RedirectResponse`, `JSONResponse`, …) passes through unchanged.
Anything else raises `chameleon_rs.fastapi.FastAPIChameleonException`.
# The `@template` decorator
``` python
@fastapi_chameleon.template('home/index.pt') # a template path
@fastapi_chameleon.template('feed.pt', 'application/xml') # mimetype=, or content_type=
@fastapi_chameleon.template('home/created.pt', status_code=201)
@fastapi_chameleon.template # no arguments
```
With no path, it renders `{module}/{function}.html`, where `{module}` is the last part of the view's module name. If there's no `.html` file in the template folder, it uses `{module}/{function}.pt`. It checks once, when the view is decorated.
# Error pages
Inside a decorated view, [not_found()](../reference/flask.not_found.md#chameleon_rs.flask.not_found) and [error()](../reference/flask.error.md#chameleon_rs.flask.error) stop the view and render an error page:
``` python
app = fastapi.FastAPI()
@app.get('/videos/{name}')
@fastapi_chameleon.template('home/index.pt')
async def video(name: str, request: fastapi.Request):
if name != 'intro':
fastapi_chameleon.not_found(path=request.url.path) # renders errors/404.pt
return {'message': name, 'videos': [name]}
response = TestClient(app).get('/videos/missing')
print(response.status_code, response.text)
```
404 Not found: /videos/missing
- `not_found(template_file='errors/404.pt', **data)` renders the page with status 404.
- `error(template_file, status_code=500, **data)` renders any error page.
- `generic_error(template_file, status_code, template_data)` is fastapi-chameleon's name and signature for [error()](../reference/flask.error.md#chameleon_rs.flask.error).
> **Warning: Template names come from your code**
>
> The names given to `@template`, [render()](../reference/flask.render.md#chameleon_rs.flask.render), [response()](../reference/flask.response.md#chameleon_rs.flask.response), [not_found()](../reference/flask.not_found.md#chameleon_rs.flask.not_found) / [error()](../reference/flask.error.md#chameleon_rs.flask.error) and [render_partial()](../reference/render_partial.md#chameleon_rs.render_partial) are looked up as the original libraries look them up, which allows `..` and absolute paths. Write them in your code; never build one from a request (a path or query parameter, a header). [chameleon_rs.starlette.Templates](../reference/starlette.Templates.md#chameleon_rs.starlette.Templates), the Jinja drop-in, refuses names that reach outside its folder, as Jinja does.
# Rendering without the decorator
``` python
html = fastapi_chameleon.render('emails/welcome.pt', user=user) # a str
html = await fastapi_chameleon.render_async('emails/welcome.pt', user=user)
resp = fastapi_chameleon.response('feed.pt', 'application/xml', items=items)
resp = await fastapi_chameleon.response_async('feed.pt', status_code=201, items=items)
```
Both raise `FastAPIChameleonException` if nothing has been configured.
# Coming from fastapi-chameleon
| fastapi-chameleon | chameleon-rs |
|----|----|
| `global_init(folder, auto_reload=False, cache_init=True)` | Still works, and takes `app=`. It configures the views only, as before, and ignores a second call. [configure()](../reference/configure.md#chameleon_rs.configure) sets views and partials together, and every call applies. |
| `@template(file, mimetype)`, [render](../reference/flask.render.md#chameleon_rs.flask.render), [response](../reference/flask.response.md#chameleon_rs.flask.response), [not_found](../reference/flask.not_found.md#chameleon_rs.flask.not_found), [generic_error](../reference/robyn.generic_error.md#chameleon_rs.robyn.generic_error) | Same. `@template` also takes `status_code=` and `content_type=`; [not_found](../reference/flask.not_found.md#chameleon_rs.flask.not_found) also takes `template_file=` and template data. |
| -- | [error()](../reference/flask.error.md#chameleon_rs.flask.error), [render_async()](../reference/robyn.render_async.md#chameleon_rs.robyn.render_async), [response_async()](../reference/robyn.response_async.md#chameleon_rs.robyn.response_async), [check_views()](../reference/flask.check_views.md#chameleon_rs.flask.check_views). |
| `FastAPIChameleonException` and the not-found / generic exceptions | Same names, now subclasses of [chameleon_rs.web.TemplateError](../reference/web.TemplateError.md#chameleon_rs.web.TemplateError), [NotFound](../reference/web.NotFound.md#chameleon_rs.web.NotFound) and [ErrorPage](../reference/web.ErrorPage.md#chameleon_rs.web.ErrorPage). |
| Views return a `dict` | Also mappings, dataclasses and Pydantic models. |
| Async views render synchronously | They render with [render_async](../reference/robyn.render_async.md#chameleon_rs.robyn.render_async); an awaitable model value is awaited where the template uses it. |
Settings are process-wide. Unlike Flask, a FastAPI app can't have a template folder of its own yet: the decorator has no request to find the app from.