A batteries-included framework for building web apps
Rust
5,008
955 commits
updated Sep 22, 2026
Topcoat is a modular, batteries-included Rust framework for building full-stack apps. It prioritizes simplicity and productivity. See Learn Topcoat to get started, or the Roadmap for what's coming next.
Early-stage and experimental. Expect breaking changes.
use topcoat::{
Result,
router::{Router, RouterBuilderDiscoverExt, page},
view::{View, component, view},
};
#[tokio::main]
async fn main() {
topcoat::start(Router::builder().discover().build()).await.unwrap();
}
#[page("/")]
async fn home() -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
hello(name: "World")
</body>
</html>
})
}
#[component]
async fn hello(name: &str) -> Result<impl View> {
Ok(view! { <h1>"Hello, " (name) "!"</h1> })
}
Topcoat renders all markup on the server: components can be async and query the database directly, eliminating all the traditional boilerplate needed for a separate API layer. Interactivity does not have to cost a round-trip, though. A $(...) expression is ordinary type-checked Rust that Topcoat evaluates on the server for the initial render and also translates to JavaScript, so it re-runs instantly in the browser. No wasm bundle, no client build step:
#[component]
async fn faq(cx: &Cx) -> Result<impl View> {
let open = signal(cx, || false);
Ok(view! {
// Runs entirely in the browser; no server round-trip.
<button @click=$(|_e| open.set(!open.get()))>"What is Topcoat?"</button>
<p :hidden=$(!open.get())>"A full-stack Rust framework."</p>
})
}
When an update does need the server, like fresh search results, mark the component as a #[shard]. Topcoat re-renders it on the server whenever one of its $(...) arguments changes and swaps the new HTML in place:
#[component]
async fn search(cx: &Cx) -> Result<impl View> {
let query = signal(cx, String::new);
Ok(view! {
<input @input=$(|e: Event| query.set(e.target.value))>
// Updates as the user types.
search_results(query: $(query.get()))
})
}
#[shard]
async fn search_results(cx: &Cx, query: String) -> Result<impl View> {
Ok(view! {
<ul>
// Your own server-side code, like a database query:
for product in search_products(cx, &query).await? {
<li>(product.name)</li>
}
</ul>
})
}
The view! macro stays true to HTML and Rust. Use familiar Rust control flow as part of your templates:
view! {
<nav>
for item in nav_items {
<a
href=(item.url)
if item.url == current_path {
aria-current="page"
class="active"
}
>
(item.label)
</a>
}
</nav>
}
Use the topcoat fmt CLI command to automatically format view! snippets (and other macros) across your codebase.
Topcoat can optionally infer your route tree from your app's module structure (without a build step):
src/
|-- app.rs -> / (and the root <html> layout)
`-- app/
|-- about.rs -> /about
|-- _marketing.rs (layout, no URL segment)
|-- _marketing/
| `-- pricing.rs -> /pricing
|-- posts.rs -> /posts
|-- posts/
| `-- id.rs -> /posts/{post_id}
`-- api/
`-- health.rs -> GET /api/health
Topcoat UI is a component library based on Tailwind inspired by shadcn/ui. Components are copied into your project via the topcoat ui CLI command, meaning you can freely change their design and functionality to fit your use case:
#[component]
async fn delete_card() -> Result<impl View> {
Ok(view! {
card(
card_header(
card_title("Delete workspace")
card_description(
"This permanently removes the workspace and all of its data."
)
)
card_footer(
attrs: attributes! { class="justify-end" },
button(variant: ButtonVariant::Ghost, "Cancel")
button(variant: ButtonVariant::Destructive, "Delete workspace")
)
)
})
}
The bundler scans your compiled binary for asset! calls, copies (or even downloads) every file into a local asset directory, and allows Topcoat to serve them efficiently with aggressive browser caching.
const FERRIS: Asset = asset!("./ferris.png");
view! { <img src=(FERRIS)> }
Topcoat also ships with utilities for web fonts and icons, as well as easy integrations for Fontsource (Google Fonts) and Iconify.
Enable the tailwind feature to integrate Tailwind into your project:
view! { <link rel="stylesheet" href=(topcoat::tailwind::stylesheet!())> }
Start here
topcoat fmt for macro bodies.Rendering
view! macro: templating syntax, control flow, conditional attributes.#[component] macro: async functions as components, with child content.attributes! macro: reusable runtime attribute fragments.class! macro: space-separated class lists from static and conditional entries.live! and emit! macros: stream slow parts of a page in after the rest, with the suspense and error_boundary components built on them.Routing
Working with requests
Cx): the value pages, layouts, and components read from.#[memoize] for per-request caching and fan-out dedup.Asset system
Client reactivity
$(...) expressions, @ event handlers, and : bind attributes.Miscellaneous
mail! macro, deliver through SMTP, file, or in-memory transports.Third-party integrations
Planned features we'd like to bring to Topcoat. Have an idea? Open an issue.
topcoat new CLI command to bootstrap pre-configured projectstopcoat-runtime)OpenAPI endpointsWebTransportRust
93.2%
TypeScript
5.8%
A batteries-included framework for building web apps
Rust
5,008
955 commits
updated Sep 22, 2026
Topcoat is a modular, batteries-included Rust framework for building full-stack apps. It prioritizes simplicity and productivity. See Learn Topcoat to get started, or the Roadmap for what's coming next.
Early-stage and experimental. Expect breaking changes.
use topcoat::{
Result,
router::{Router, RouterBuilderDiscoverExt, page},
view::{View, component, view},
};
#[tokio::main]
async fn main() {
topcoat::start(Router::builder().discover().build()).await.unwrap();
}
#[page("/")]
async fn home() -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
hello(name: "World")
</body>
</html>
})
}
#[component]
async fn hello(name: &str) -> Result<impl View> {
Ok(view! { <h1>"Hello, " (name) "!"</h1> })
}
Topcoat renders all markup on the server: components can be async and query the database directly, eliminating all the traditional boilerplate needed for a separate API layer. Interactivity does not have to cost a round-trip, though. A $(...) expression is ordinary type-checked Rust that Topcoat evaluates on the server for the initial render and also translates to JavaScript, so it re-runs instantly in the browser. No wasm bundle, no client build step:
#[component]
async fn faq(cx: &Cx) -> Result<impl View> {
let open = signal(cx, || false);
Ok(view! {
// Runs entirely in the browser; no server round-trip.
<button @click=$(|_e| open.set(!open.get()))>"What is Topcoat?"</button>
<p :hidden=$(!open.get())>"A full-stack Rust framework."</p>
})
}
When an update does need the server, like fresh search results, mark the component as a #[shard]. Topcoat re-renders it on the server whenever one of its $(...) arguments changes and swaps the new HTML in place:
#[component]
async fn search(cx: &Cx) -> Result<impl View> {
let query = signal(cx, String::new);
Ok(view! {
<input @input=$(|e: Event| query.set(e.target.value))>
// Updates as the user types.
search_results(query: $(query.get()))
})
}
#[shard]
async fn search_results(cx: &Cx, query: String) -> Result<impl View> {
Ok(view! {
<ul>
// Your own server-side code, like a database query:
for product in search_products(cx, &query).await? {
<li>(product.name)</li>
}
</ul>
})
}
The view! macro stays true to HTML and Rust. Use familiar Rust control flow as part of your templates:
view! {
<nav>
for item in nav_items {
<a
href=(item.url)
if item.url == current_path {
aria-current="page"
class="active"
}
>
(item.label)
</a>
}
</nav>
}
Use the topcoat fmt CLI command to automatically format view! snippets (and other macros) across your codebase.
Topcoat can optionally infer your route tree from your app's module structure (without a build step):
src/
|-- app.rs -> / (and the root <html> layout)
`-- app/
|-- about.rs -> /about
|-- _marketing.rs (layout, no URL segment)
|-- _marketing/
| `-- pricing.rs -> /pricing
|-- posts.rs -> /posts
|-- posts/
| `-- id.rs -> /posts/{post_id}
`-- api/
`-- health.rs -> GET /api/health
Topcoat UI is a component library based on Tailwind inspired by shadcn/ui. Components are copied into your project via the topcoat ui CLI command, meaning you can freely change their design and functionality to fit your use case:
#[component]
async fn delete_card() -> Result<impl View> {
Ok(view! {
card(
card_header(
card_title("Delete workspace")
card_description(
"This permanently removes the workspace and all of its data."
)
)
card_footer(
attrs: attributes! { class="justify-end" },
button(variant: ButtonVariant::Ghost, "Cancel")
button(variant: ButtonVariant::Destructive, "Delete workspace")
)
)
})
}
The bundler scans your compiled binary for asset! calls, copies (or even downloads) every file into a local asset directory, and allows Topcoat to serve them efficiently with aggressive browser caching.
const FERRIS: Asset = asset!("./ferris.png");
view! { <img src=(FERRIS)> }
Topcoat also ships with utilities for web fonts and icons, as well as easy integrations for Fontsource (Google Fonts) and Iconify.
Enable the tailwind feature to integrate Tailwind into your project:
view! { <link rel="stylesheet" href=(topcoat::tailwind::stylesheet!())> }
Start here
topcoat fmt for macro bodies.Rendering
view! macro: templating syntax, control flow, conditional attributes.#[component] macro: async functions as components, with child content.attributes! macro: reusable runtime attribute fragments.class! macro: space-separated class lists from static and conditional entries.live! and emit! macros: stream slow parts of a page in after the rest, with the suspense and error_boundary components built on them.Routing
Working with requests
Cx): the value pages, layouts, and components read from.#[memoize] for per-request caching and fan-out dedup.Asset system
Client reactivity
$(...) expressions, @ event handlers, and : bind attributes.Miscellaneous
mail! macro, deliver through SMTP, file, or in-memory transports.Third-party integrations
Planned features we'd like to bring to Topcoat. Have an idea? Open an issue.
topcoat new CLI command to bootstrap pre-configured projectstopcoat-runtime)OpenAPI endpointsWebTransportRust
93.2%
TypeScript
5.8%