Skip to content

Tutorial: Build a Notes App

In this tutorial, we will build a complete, database-backed Notes application from scratch in under 10 minutes. You will learn how to route pages, fetch data, handle form submissions securely, and use dynamic URLs.

1. Setup the Project

First, use the CLI to scaffold a new project. Choose SQLite when prompted.

albedo init notes_app
cd notes_app

2. Create the Database Model

Albedo configures SQLAlchemy for you. Let's define what a Note looks like in the database.

Create a new file at app/models/note.py:

from sqlalchemy import Column, Integer, String, Text
from app.database import Base

class Note(Base):
    __tablename__ = "notes"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    content = Column(Text)

Important: Open alembic/env.py and import your model so Alembic can see it! Add this near the top:

from app.models.note import Note

Now, generate and run the migration to create the table:

alembic revision --autogenerate -m "create notes table"
alembic upgrade head

3. The Layout

Let's create a clean container for our application. Edit app/pages/_layout.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Albedo Notes</title>
    <link rel="stylesheet" href="/static/styles.css">
    <style>
        .container { max-width: 600px; margin: 2rem auto; }
        .card { background: white; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    </style>
</head>
<body style="background: #f3f4f6; font-family: sans-serif;">
    <main class="container">
        <h1>📝 My Notes</h1>
        {{ content | safe }}
    </main>
</body>
</html>

4. The Home Page (Read & Create)

We want the root page (/) to display all notes and show a form to create a new one.

Edit app/pages/page.py to handle the data fetching (loader) and form submission (action):

from fastapi import Form
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from typing import Annotated

from app.models.note import Note

# GET: Fetch all notes from the database
def loader(db: Session):
    notes = db.query(Note).order_by(Note.id.desc()).all()
    return {"notes": notes}

# POST: Save a new note to the database
def action(db: Session, title: Annotated[str, Form()], content: Annotated[str, Form()]):
    new_note = Note(title=title, content=content)
    db.add(new_note)
    db.commit()

    # Refresh the page to see the new note
    return RedirectResponse(url="/", status_code=303)

Now, build the UI in app/pages/page.html:

<!-- The Form -->
<div class="card" style="background: #e5e7eb;">
    <form method="POST" action="/">
        <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

        <input type="text" name="title" placeholder="Note Title" required style="width: 100%; margin-bottom: 0.5rem; padding: 0.5rem;">
        <textarea name="content" placeholder="Write something..." required style="width: 100%; margin-bottom: 0.5rem; padding: 0.5rem;"></textarea>

        <button type="submit" style="background: #2563eb; color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer;">
            Save Note
        </button>
    </form>
</div>

<!-- The Notes List -->
{% for note in notes %}
<div class="card">
    <h2 style="margin-top: 0;">{{ note.title }}</h2>
    <p>{{ note.content }}</p>

    <!-- Delete Button (Points to a dynamic route we will create next) -->
    <form method="POST" action="/delete/{{ note.id }}" style="text-align: right;">
        <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
        <button type="submit" style="color: red; background: none; border: none; cursor: pointer;">Delete</button>
    </form>
</div>
{% else %}
<p>No notes yet! Write one above.</p>
{% endfor %}

5. Deleting (Dynamic Routes)

Notice the delete button submits a POST request to /delete/{{ note.id }}. Let's create that dynamic route.

Create the nested folders: app/pages/delete/[id]/

Then create app/pages/delete/[id]/page.py:

from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.models.note import Note

# We only need an action since we are just processing a deletion form, not rendering a page
def action(db: Session, id: int):
    note = db.query(Note).filter(Note.id == id).first()
    if note:
        db.delete(note)
        db.commit()

    return RedirectResponse(url="/", status_code=303)

6. Run It!

Start the server:

albedo dev

Open your browser to http://127.0.0.1:8000. You can now create and delete notes, all secured by CSRF tokens and powered by dynamic folder routing!