I tried building a web app in Rust last year. Not just an API — a real web app with login pages, search, and real-time updates. I spent two weeks wiring up Axum on the backend, React on the frontend, a REST API layer to connect them, CORS middleware because of course they live on different ports, and a build pipeline that involved Node.js, Webpack, and more configuration files than my entire Rust project.

It worked. But it was exhausting. Every feature required touching at least three files across two languages. And somewhere around week three, I asked myself: why can’t I just write the whole thing in Rust?

Turns out the tokio-rs team had the same question. Their answer is Topcoat — a new full-stack Rust framework that lets you build server-rendered web apps with client-side reactivity, all in a single Rust codebase. No React, no Vue, no separate API layer, no WASM bundles. Just Rust, from your database query to your button click handler.

I’ve been playing with it for a few days, and I think it’s one of the most exciting developments in the Rust web ecosystem this year. Let me walk you through building your first Topcoat app from scratch — you’ll have a working full-stack Rust application running in about 15 minutes.

Programming code displayed on a computer screen with colorful syntax highlighting
Image: Bloxgros via Wikimedia Commons (CC BY-SA 4.0)

Before We Start: What Makes Topcoat Different

Topcoat is built by the tokio-rs team — the same people behind Tokio, the async runtime that powers most of the Rust web ecosystem. It’s a batteries-included framework that prioritizes simplicity and productivity. Think of it as what you’d get if someone built a Rails-like experience for Rust, but with the type safety and performance you’d expect from the language. If you’ve been exploring Rust through projects like building and running the Linux 0.11 rewrite in Rust, you already know what the language can do at the systems level. Topcoat proves it can do full-stack web too.

The core idea is straightforward: you write all your web app logic in Rust, on the server. HTML templates, database queries, authentication, form handling, and even client-side interactivity — all in one language, all in one codebase. But Topcoat has two clever tricks up its sleeve that make this actually practical:

  • Signals — reactive values that can update the DOM without a server round-trip. A $(...) expression in your template is real, type-checked Rust that Topcoat translates to JavaScript automatically. The initial render happens on the server, but updates run instantly in the browser.
  • Shards — components that re-render on the server when their arguments change. Think LiveView or Hotwire, but with Rust’s async performance and compile-time safety.

No WASM, no build step for the frontend, no API contract to maintain. Just Rust functions that return HTML. This is a fundamentally different approach from runtimes like Ant, the 9MB JavaScript runtime that cold-starts in 5ms — while Ant optimizes the JS runtime itself, Topcoat eliminates the need for a separate frontend runtime entirely.

Prerequisites

Before we start, make sure you have:

  • Rust toolchain installed (1.80+). If you don’t have it: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Cargo — comes with Rust
  • A terminal and a text editor

That’s it. No Node.js, no npm, no WASM toolchain, no Docker. Just Rust.

Step 1: Create a New Project

Start with a fresh Cargo binary:

cargo new topcoat-hello-world
cd topcoat-hello-world

This creates a minimal Rust project with src/main.rs and Cargo.toml.

Step 2: Add Dependencies

Add Topcoat and Tokio as dependencies:

cargo add topcoat
cargo add tokio --features rt-multi-thread,macros

The first build takes a moment — Cargo pulls in Axum (for the HTTP server), tower (middleware), and the reactive runtime — but subsequent builds are fast thanks to incremental compilation.

Step 3: Write Your First Page

Replace src/main.rs with this:

use topcoat::{
    Result,
    router::{Router, RouterBuilderDiscoverExt, page},
    view::{component, view},
};

#[tokio::main]
async fn main() {
    topcoat::start(Router::builder().discover().build()).await.unwrap();
}

#[page("/")]
async fn home() -> Result {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                <title>"Hello world"</title>
                topcoat::dev::script()
            </head>
            <body>
                hello(name: "World")
            </body>
        </html>
    }
}

#[component]
async fn hello(name: &str) -> Result {
    view! {
        <h1>"Hello, " (name) "!"</h1>
    }
}

Here’s what each piece does:

  • #[page("/")] — registers this function as a route handler for the root URL. Routes are async functions that return HTML.
  • view! — Topcoat’s templating macro. It looks like writing HTML, but every tag and expression is compiled Rust, type-checked and safe.
  • topcoat::dev::script() — injects the development client for hot reload. Your page auto-refreshes when you save changes.
  • #[component] — creates a reusable UI component. The hello function takes a parameter and returns HTML.
  • (name) inside the view! macro — inserts the variable value into the HTML output.

You can actually run this right now: cargo run starts the server on http://127.0.0.1:3000. No CLI needed for basic serving.

Step 4: Install the CLI and Start Dev Server

For day-to-day development, install the Topcoat CLI. It enables hot reloading, asset bundling, and source formatting:

cargo install topcoat-cli

This installs a topcoat executable. Once it’s ready, start the dev server:

topcoat dev

Open http://127.0.0.1:3000 and you’ll see Hello, World!. The dev server watches your source directory and rebuilds on every save — no manual restarts, no file watcher configuration.

To change the bind address: HOST=0.0.0.0 PORT=8080 topcoat dev

Step 5: Add Client-Side Interactivity

Here’s where Topcoat breaks from the traditional server-rendered mold. Let’s add a toggle button that responds instantly in the browser — no network request needed:

#[component]
async fn toggle_demo() -> Result {
    view! {
        signal open = false;

        <button @click=$(|_e| open.set(!open.get()))>
            "What is Topcoat?"
        </button>

        <p :hidden=$(!open.get())>
            "A fullstack Rust framework."
        </p>
    }
}

Add toggle_demo() to your home page body and rebuild. Here’s the breakdown:

  • signal open = false declares a reactive signal initialized to false
  • @click=$(|_e| open.set(!open.get())) is an event handler written in Rust. Click the button, the signal toggles, the DOM updates — all client-side
  • :hidden=$(!open.get()) binds the hidden attribute to the signal’s inverse value. When open is false, the paragraph stays hidden

The mind-blowing part: that $(...) expression is real Rust code. The compiler type-checks it at build time. If you pass a wrong argument type, the build fails — not at runtime in a browser console. This alone makes me sleep better compared to debugging JavaScript at 2 AM. It’s the same principle behind tools like Destructive Command Guard for AI coding tools — compile-time guardrails beat runtime panic every time.

Step 6: Server-Side Updates with Shards

Some things need the server — search results, database lookups, authentication. Topcoat handles this with shards, which are components that re-render on the server when their signal arguments change:

#[component]
async fn search_page() -> Result {
    view! {
        signal query = String::new();

        <input @input=$(|e: Event| query.set(e.target.value)) />

        search_results(query: $(query.get()))
    }
}

#[shard]
async fn search_results(cx: &Cx, query: String) -> Result {
    view! {
        <ul>
            for product in search_products(cx, &query).await? {
                <li>(product.name)</li>
            }
        </ul>
    }
}

The #[shard] attribute marks the component for server-side re-rendering. As the user types, the signal updates, which triggers a server request that re-renders only the search results and swaps the HTML. No page reload, no client framework, no API contract to maintain.

This pattern reminds me of Rails Hotwire or Phoenix LiveView — but with Rust’s type system and async performance under the hood.

Step 7: Module-Based Routing

For larger apps, Topcoat can auto-discover your route tree from your module structure:

src/
|-- app.rs              -> /            (root layout)
|-- app/
    |-- about.rs        -> /about
    |-- posts.rs        -> /posts
    |-- posts/
    |   `-- id.rs       -> /posts/{post_id}
    `-- api/
        `-- health.rs   -> GET /api/health

No route registry file. No dynamic dispatch. The module structure is your route table. Layout modules prefixed with _ (like _marketing.rs) wrap sub-routes without adding a URL segment.

The bundler is equally slick — declare assets with asset!("./ferris.png") and Topcoat serves them with content-hashed URLs for aggressive caching:

const FERRIS: Asset = asset!("./ferris.png");
view! { <img src=(FERRIS) /> }

There’s also built-in Tailwind support. Enable the tailwind feature and add one line to your template — no Node.js or PostCSS setup needed:

<link rel="stylesheet" href=(topcoat::tailwind::stylesheet!()) />

Where Topcoat Fits — and Where It’s Going

Topcoat isn’t competing with Axum or Actix — it actually uses Axum under the hood. Instead, it’s competing with the combination of a backend framework, a frontend framework, an API layer, and a build pipeline. For solo developers or small teams shipping full-stack apps in Rust, that combination is exactly the problem worth solving.

It’s still early — the README explicitly warns about breaking changes, and the crate is on version 0.x. But 748 commits from the tokio-rs team is a strong signal. The documentation on docs.rs covers sessions, cookies, app context, memoization, and the full expression language. The team is active on the Tokio Discord.

If you’re evaluating full-stack Rust and wondering if there’s a way to avoid maintaining two codebases, give Topcoat a try. Create a project, install the CLI, and build a page with a signal. Just like companies are ditching paid AI APIs for self-hosted open-source models, the best way to evaluate a new framework is to actually run it yourself. Even if you don’t ship production with it, the ideas it brings — server-driven UI with compile-time safety — are worth understanding.

Because honestly? The future of web development might not be another JavaScript framework. It might be no JavaScript at all.

Filed under Tech & Gadgets
Last Update: July 18, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted