The Type Is the Intent
Route functions return values. Chirp handles content negotiation based on the return type -- nomake_response(), no jsonify(), no explicit content-type wiring.
return "Hello" # -> 200, text/html
return {"users": [...]} # -> 200, application/json
return Template("page.html", title="Home") # -> 200, rendered via kida
return Template.inline("<h1>{{ t }}</h1>", t="Hi") # -> 200, from string
return Fragment("page.html", "results", items=x) # -> 200, rendered block
return Stream("dashboard.html", **async_ctx) # -> 200, streamed HTML
return EventStream(generator()) # -> SSE stream
return Response(body=b"...", status=201) # -> explicit control
return Redirect("/login") # -> 302
InlineTemplate (Prototyping)
Renders a template from a string instead of a file. Useful for prototyping and scripts where you don't want to set up atemplates/directory:
from chirp import Template
@app.route("/")
def index():
return Template.inline("<h1>{{ greeting }}</h1>", greeting="Hello, world!")
Template.inline() returns an InlineTemplate instance. It works through content negotiation without requiring a template_dirto be configured.
Note
InlineTemplate is a prototyping shortcut. app.check() will emit a warning for routes that return it. Replace with file-based Templatebefore production.
Template
Renders a full template via kida:
from chirp import Template
@app.route("/")
def index():
return Template("index.html", title="Home", items=items)
The first argument is the template path (relative to your template_dir). Everything else becomes template context.
Fragment
Renders a named block from a template, without rendering the full page:
from chirp import Fragment
@app.route("/search")
def search(request: Request):
results = do_search(request.query.get("q", ""))
if request.is_fragment:
return Fragment("search.html", "results_list", results=results)
return Template("search.html", results=results)
This is Chirp's key differentiator. Same template, same data, different scope. See Fragments for the full story.
Page
Auto-detects whether to return a full page or a fragment based on the request:
from chirp import Page
@app.route("/search")
def search(request: Request):
results = do_search(request.query.get("q", ""))
return Page("search.html", "results_list", results=results)
Page is sugar over the if request.is_fragmentpattern. If the request comes from htmx, it renders the block. Otherwise, it renders the full template.
Stream
Progressive HTML rendering. The browser receives the page shell immediately and content fills in as data becomes available:
from chirp import Stream
@app.route("/dashboard")
async def dashboard():
return Stream("dashboard.html",
header=site_header(),
stats=await load_stats(),
activity=await load_activity(),
)
See Streaming HTML for details.
EventStream
Server-Sent Events. Push data to the browser over a persistent connection:
from chirp import EventStream, Fragment
@app.route("/notifications")
async def notifications():
async def stream():
async for event in notification_bus.subscribe():
yield Fragment("components/notification.html", event=event)
return EventStream(stream())
The generator yields values (strings, dicts, Fragments, or SSEEvents). See Server-Sent Events for details.
Response
Explicit control over the HTTP response:
from chirp import Response
@app.route("/api/create")
async def create():
return Response(body=b'{"id": 42}', status=201).with_header(
"Content-Type", "application/json"
)
Response supports a chainable .with_*()API. See Request & Response.
Redirect
from chirp import Redirect
@app.route("/old-page")
def old_page():
return Redirect("/new-page") # 302 by default
Strings and Dicts
Plain strings are returned astext/html. Dicts are serialized as JSON:
@app.route("/hello")
def hello():
return "Hello, World!" # text/html, 200
@app.route("/api/status")
def status():
return {"status": "ok"} # application/json, 200
ValidationError
Returns a 422 response with a rendered fragment, designed for form validation:
from chirp import ValidationError
@app.route("/submit", methods=["POST"])
async def submit(request: Request):
form = await request.form()
errors = validate(form)
if errors:
return ValidationError("form.html", "form_errors", errors=errors)
# ... process valid form
OOB (Out-of-Band)
Sends multiple fragment updates in a single response -- the main content plus additional out-of-band swaps:
from chirp import OOB, Fragment
@app.route("/update")
def update():
return OOB(
Fragment("page.html", "main_content", data=data),
Fragment("page.html", "sidebar", stats=stats),
Fragment("page.html", "notification_count", count=count),
)
Combined with htmx's hx-swap-oob, this updates multiple parts of the page in one request.
Next Steps
- Fragments -- Deep dive into fragment rendering
- Rendering -- Template rendering in detail
- Request & Response -- The chainable Response API