Build a Full-Stack Python Web App Without JavaScript? Here's How with Reflex

Akram Chauhan
Akram Chauhan
11 min read190 views
Build a Full-Stack Python Web App Without JavaScript? Here's How with Reflex

Tired of juggling Python for your backend and then having to switch gears to a completely different world for the frontend? You know the drill. You build this beautiful logic in Python, and then you hit a wall: you need a user interface. Suddenly, you're wrestling with JavaScript, React, Vue, or Angular, and it feels like you're learning a whole new language just to display some data.

I’ve been there, and it can be frustrating.

But what if I told you that you could build a modern, reactive, multi-page web app with a real-time database… entirely in Python? No JavaScript required. Seriously.

That's the magic of a framework called Reflex. It lets you build both the frontend and backend in one unified Python codebase. Today, we're going to do just that. We're not building a simple "Hello, World!" app. We're going all in and creating a complete notes-management dashboard with two pages, a live database, filtering, sorting, and even some cool analytics.

Let's dive in and build this thing together.

Step 1: Setting Up Our Workshop

First things first, we need to get our environment ready. Think of this as clearing your workbench before starting a new project. We'll be doing this inside a Colab notebook to make it super easy for anyone to follow along, but the process is pretty much the same on your local machine.

We just need to create a directory for our project and install Reflex. It’s a one-liner that gets everything we need.

import os, subprocess, sys, pathlib
APP = "reflex_colab_advanced"
os.makedirs(APP, exist_ok=True)
os.chdir(APP)
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "reflex==0.5.9"])

With that, our workspace is prepped and the tools are laid out. Simple as that.

Step 2: The App's ID Card (Configuration)

Every application needs a little bit of configuration. This file, rxconfig.py, is like the app's ID card. It tells Reflex its name and, most importantly, where to store its data.

We’re going to use a local SQLite database. If you haven't used SQLite before, you'll love it. It's a lightweight, serverless database that lives in a single file right inside our project. It's perfect for getting projects off the ground without the headache of setting up a big database server.

Here’s our entire configuration file. It’s clean and to the point.

rxconfig = """
import reflex as rx

class Config(rx.Config):
    app_name = "reflex_colab_advanced"
    db_url = "sqlite:///reflex.db"

config = Config()
"""
pathlib.Path("rxconfig.py").write_text(rxconfig)

We’ve basically just told Reflex, "Our app is called reflex_colab_advanced, and please save all our data in a file named reflex.db." That’s it! Configuration, done.

Step 3: The Brains of the Operation (Models & State)

Okay, this is where the real magic starts to happen. We're going to define the entire logic of our application. This includes what our data looks like and how the app should behave when a user interacts with it. In Reflex, this all lives in a single State class.

Think of the State class as the app's central nervous system. It holds the app's memory (like what the user is typing in a search box) and controls all the actions (like adding or deleting a note).

Let's break it down.

The Data Model

First, we need to define the structure of a "note." What information does each note contain? For our app, it’s pretty simple: some text content, an optional tag, and a boolean to track if it's done.

In Reflex, we define this with a simple Python class that inherits from rx.Model.

import reflex as rx

class Note(rx.Model, table=True):
    content: str
    tag: str = "general"
    done: bool = False

The table=True part is the key. It tells Reflex to automatically create a database table for this model. It's that easy. No writing SQL schemas by hand.

The State Class

Now for the big one: the State class. This class will manage everything. User input, search queries, filters, and all the database operations. It might look like a lot of code at first, but let's walk through it. It's surprisingly intuitive.

class State(rx.State):
    # Basic variables to hold UI state
    user: str = ""
    search: str = ""
    tag_filter: str = "all"
    sort_desc: bool = True
    new_content: str = ""
    new_tag: str = "general"
    toast_msg: str = ""

    # Event handlers for simple state changes
    def set_user(self, v: str):
        self.user = v

    def set_search(self, v: str):
        self.search = v

    def set_tag_filter(self, v: str):
        self.tag_filter = v

    def set_new_content(self, v: str):
        self.new_content = v

    def set_new_tag(self, v: str):
        self.new_tag = v

    def toggle_sort(self):
        self.sort_desc = not self.sort_desc

    # Asynchronous event handlers for database actions
    async def add_note(self):
        if self.new_content.strip():
            with rx.session() as session:
                session.add(
                    Note(content=self.new_content.strip(), tag=self.new_tag.strip() or "general")
                )
                session.commit()
            self.new_content = ""; self.toast_msg = "Note added"

    async def toggle_done(self, note_id: int):
        with rx.session() as session:
            note = session.get(Note, note_id)
            if note:
                note.done = not note.done
                session.commit()

    async def delete_note(self, note_id: int):
        with rx.session() as session:
            note = session.get(Note, note_id)
            if note:
                session.delete(note)
                session.commit()
        self.toast_msg = "Deleted"

    async def clear_done(self):
        with rx.session() as session:
            notes_to_delete = session.query(Note).filter(Note.done == True).all()
            for note in notes_to_delete:
                session.delete(note)
            session.commit()
        self.toast_msg = "Cleared done notes"

    # Computed properties that automatically update
    @rx.var
    def notes_filtered(self) -> list[Note]:
        with rx.session() as session:
            query = session.query(Note)
            q = self.search.lower()
            if q:
                query = query.filter(Note.content.contains(q) | Note.tag.contains(q))
            if self.tag_filter != "all":
                query = query.filter(Note.tag == self.tag_filter)
            
            order = Note.id.desc() if self.sort_desc else Note.id.asc()
            return query.order_by(order).all()

    @rx.var
    def stats(self) -> dict:
        with rx.session() as session:
            total = session.query(Note).count()
            done = session.query(Note).filter(Note.done == True).count()
            
            tag_counts = {}
            notes = session.query(Note).all()
            for n in notes:
                tag_counts[n.tag] = tag_counts.get(n.tag, 0) + 1
            top_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)[:5]
            
            return {"total": total, "done": done, "pending": total - done, "tags": top_tags}

Here’s the breakdown:

  • Simple Variables: user, search, etc., are just variables that hold the current state of our UI. When a user types in the search box, the search variable will update.
  • Event Handlers: Methods like add_note and delete_note are the functions that run when a user clicks a button. Notice they are async because database operations can take a moment, and we don't want to freeze the app.
  • Computed Properties (@rx.var): This is where Reflex shines. notes_filtered and stats aren't just regular methods. They are reactive. Whenever search, tag_filter, or the notes in the database change, Reflex automatically re-runs these functions and updates the UI. You don't have to manually refresh anything. It just works.

Step 4: Building with LEGOs (Our UI Components)

Now that we have our logic, let's build the visual parts. I like to think of this as creating a set of reusable LEGO bricks. Instead of building one giant, messy page, we'll create small, self-contained components for the sidebar, the stats cards, each individual note, and so on.

Reflex gives us simple Python functions like rx.vstack (vertical stack), rx.hstack (horizontal stack), rx.button, and rx.text to build our UI. It feels a lot like writing Python, not wrestling with HTML and CSS.

def sidebar():
    return rx.vstack(
        rx.heading("RC Advanced", size="6"),
        rx.link("Dashboard", href="/"),
        rx.link("Notes Board", href="/board"),
        rx.text("User"),
        rx.input(placeholder="your name", value=State.user, on_change=State.set_user),
        spacing="3",
        width="15rem",
        padding="1rem",
        border_right="1px solid #eee"
    )

def stats_cards():
    s = State.stats
    return rx.hstack(
        rx.box(rx.text("Total"), rx.heading(str(s["total"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
        rx.box(rx.text("Done"), rx.heading(str(s["done"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
        rx.box(rx.text("Pending"), rx.heading(str(s["pending"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
        spacing="4"
    )

def tag_pill(tag: str, count: int = 0):
    return rx.badge(
        f"{tag} ({count})" if count else tag,
        on_click=State.set_tag_filter(tag),
        cursor="pointer",
        color_scheme=rx.cond(State.tag_filter == tag, "blue", "gray")
    )

def tags_bar():
    s = State.stats
    tags = [("all", s["total"])] + s["tags"]
    return rx.hstack(*[tag_pill(t[0], t[1]) for t in tags], spacing="2", wrap="wrap")

def note_row(note: Note):
    return rx.hstack(
        rx.hstack(
            rx.checkbox(is_checked=note.done, on_change=lambda checked: State.toggle_done(note.id)),
            rx.text(note.content, text_decoration=rx.cond(note.done, "line-through", "none")),
        ),
        rx.badge(note.tag, color_scheme="green"),
        rx.button(rx.icon("trash"), on_click=lambda: State.delete_note(note.id), color_scheme="red", size="1"),
        justify="between",
        width="100%"
    )

def notes_list():
    return rx.vstack(
        rx.foreach(State.notes_filtered, note_row),
        spacing="2",
        width="100%"
    )

Notice how everything is connected directly to our State class. The user input on_change calls State.set_user. The note's text has a line through it if note.done is true. This tight link between the UI and the state is what makes the app feel so alive and responsive.

Step 5: Assembling the Final Product

We've built our LEGO bricks. Now it's time to put them together to create our two pages: the Dashboard and the Notes Board.

This is the easy part. We just arrange our components on the page.

def dashboard_page():
    return rx.hstack(
        sidebar(),
        rx.box(
            rx.heading("Dashboard", size="8"),
            rx.cond(
                State.user != "",
                rx.text(f"Hi {State.user}, here is your activity"),
                rx.text(""),
            ),
            rx.vstack(
                rx.suspense(stats_cards, fallback=rx.text("Loading stats...")),
                rx.suspense(tags_bar, fallback=rx.text("Loading tags...")),
                spacing="4"
            ),
            padding="2rem",
            width="100%"
        ),
        width="100%"
    )

def board_page():
    return rx.hstack(
        sidebar(),
        rx.box(
            rx.heading("Notes Board", size="8"),
            rx.hstack(
                rx.input(placeholder="search...", value=State.search, on_change=State.set_search, width="50%"),
                rx.button("Toggle sort", on_click=State.toggle_sort),
                rx.button("Clear done", on_click=State.clear_done, color_scheme="red"),
                spacing="2"
            ),
            rx.hstack(
                rx.input(placeholder="note content", value=State.new_content, on_change=State.set_new_content, width="60%"),
                rx.input(placeholder="tag", value=State.new_tag, on_change=State.set_new_tag, width="20%"),
                rx.button("Add", on_click=State.add_note),
                spacing="2"
            ),
            rx.cond(
                State.toast_msg != "",
                rx.callout(State.toast_msg, icon="info")
            ),
            rx.suspense(notes_list, fallback=rx.text("Loading notes...")),
            padding="2rem",
            width="100%"
        ),
        width="100%"
    )

# Add the pages to the app and compile.
app = rx.App()
app.add_page(dashboard_page, route="/", title="RC Dashboard")
app.add_page(board_page, route="/board", title="Notes Board")

The rx.suspense component is a nice touch. It tells Reflex to show a "Loading..." message while it's fetching the data from our stats or notes_filtered computed properties. This makes the user experience much smoother.

Finally, we create the app instance and add our pages with their routes.

And... We're Live!

That's it. We've defined our data, our logic, and our UI. The last step is to tell Reflex to run the app.

# This part is for running in Colab, but locally you'd just run `reflex run`
# in your terminal.
# (Code for running the app is omitted here for blog post clarity, but is in the original source)

And just like that, you have a fully functional, real-time, multi-page web application. You can add notes, delete them, mark them as done, filter by tags, search, and see your stats update instantly. All in pure Python.

What we just built is pretty incredible when you think about it. We have a frontend, a backend, and a database all working together seamlessly, managed by a single Python codebase. For Python developers who want to build for the web without getting lost in the JavaScript ecosystem, tools like Reflex are an absolute game-changer.

So, go ahead and play with the code. Tweak it, break it, and make it your own. The best way to learn is by doing. What will you build next?

Stay Updated

Get the latest articles and insights delivered straight to your inbox.

We respect your privacy. Unsubscribe at any time.

Aicosoft

AI & Technology News, Insights & Innovation

AICOSOFT delivers cutting-edge AI news, technology breakthroughs, and innovation insights. Stay informed about artificial intelligence, machine learning, robotics, and the latest tech trends shaping tomorrow.

Connect With Us

© 2026 Aicosoft. All rights reserved.