Quickstart

Build your first Chirp application in 5 minutes

3 min read 663 words

Prerequisites

Before You Start

0/2 complete

Scaffold a Project

The fastest way to start is thechirp newcommand:

chirp new myapp
cd myapp
python app.py

Open http://127.0.0.1:8000in your browser.

chirp new myappnow generates an auth-ready v2 layout:

  • app.pywith sessions, auth, CSRF, and security headers middleware
  • models.pywith a demo user model + password hashing
  • pages/ filesystem routes (/, /login, /dashboard)
  • static/style.css
  • tests/with auth flow tests

The scaffold runs in development mode by default and readsCHIRP_SECRET_KEY. Before production, set a strong secret and run with production settings.

export CHIRP_SECRET_KEY="$(python - <<'PY'
import secrets
print(secrets.token_urlsafe(48))
PY
)"

For an even smaller starting point:

chirp new myapp --minimal

This generates only app.py and templates/index.html.

Hello World (Manual)

You can also create a project by hand. Create a file calledapp.py:

from chirp import App

app = App()

@app.route("/")
def index():
    return "Hello, World!"

app.run()

Run it:

python app.py

Open http://127.0.0.1:8000in your browser. Five lines to hello world.

Add Templates

Create atemplates/ directory and add base.html:

<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

Add templates/index.html:

{% extends "base.html" %}

{% block content %}
  <h1>{{ title }}</h1>
  <p>Welcome to my Chirp app.</p>
{% endblock %}

Update app.py:

from chirp import App, Template

app = App()

@app.route("/")
def index():
    return Template("index.html", title="Home")

app.run()

Route functions return values. Template tells the framework to render index.htmlwith the given context via kida.

Fragment Rendering

This is where Chirp diverges from Flask. Add a search feature that works as both a full page and an htmx fragment.

Addtemplates/search.html:

{% extends "base.html" %}

{% block content %}
  <h1>Search</h1>
  <input type="search" name="q"
         hx-get="/search" hx-target="#results" hx-trigger="input changed delay:300ms">

  {% block results %}
    <div id="results">
      {% for item in results %}
        <p>{{ item }}</p>
      {% endfor %}
    </div>
  {% endblock %}
{% endblock %}

Update app.py:

from chirp import App, Template, Fragment, Request

app = App()

ITEMS = ["apple", "banana", "cherry", "date", "elderberry"]

@app.route("/")
def index():
    return Template("index.html", title="Home")

@app.route("/search")
def search(request: Request):
    q = request.query.get("q", "")
    results = [i for i in ITEMS if q.lower() in i.lower()] if q else ITEMS

    if request.is_fragment:
        return Fragment("search.html", "results", results=results)
    return Template("search.html", title="Search", results=results)

app.run()

Full page navigation renders everything. An htmx request renders just the resultsblock. Same template, same data, different scope.

Add htmx

To make the fragment rendering work, include htmx in yourbase.html:

<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
  <script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

Now the search input sends requests to /search via htmx, and Chirp responds with just the resultsblock -- no full page reload, no separate partials directory, no JavaScript.

Live Updates in 5 Minutes

Add real-time updates with SSE and view transitions:

  1. 1

    Install Chirp

    If not already installed:pip install bengal-chirp

  2. 2

    Extend the boost layout

    Add the layout and SSE scope to your template:

    {% extends "chirp/layouts/boost.html" %}
    {% block content %}
      <ol>
        {% for item in items %}
        <li>{{ item.title }}</li>
        {% endfor %}
      </ol>
    {% endblock %}
    {% block sse_scope %}
      {% from "chirp/sse.html" import sse_scope %}
      {{ sse_scope("/events") }}
    {% endblock %}
    
  3. 3

    Stream fragments from your route

    from chirp import EventStream, Fragment
    
    @app.route("/events")
    def events():
        def stream():
            yield Fragment("my_template.html", "live_block", items=...)
        return EventStream(stream)
    
  4. 4

    Run chirp check

    Runchirp check myapp:appto catch SSE scope violations before opening the browser.

See View Transitions + OOB for the full pattern.

Next Steps