No. 1
Reactivity without boilerplate
Write browser interactions as type-checked Rust. Topcoat translates them to JavaScript, so a toggle never waits on the server.
Est. MMXXVI · A Tokio Project
The full full-stack framework for Rust.
Server-rendered, reactive web apps, written entirely in Rust. No WebAssembly bundle, no separate API layer, no client build step. Sensible outerwear for any weather.
This dial is a Topcoat signal. It changes the weather in your browser, with no trip to the server.
Hello, world, in one file.
A page is an async function. A component is an async function. Both return markup from the view! macro, and components are called from markup with named arguments.
Because everything renders on the server, a component can query your database directly. There is no API layer to write between your data and your HTML.
use topcoat::{
Result,
context::Cx,
router::{module_router, page},
view::{View, component, view},
};
#[tokio::main]
async fn main() {
topcoat::start(module_router!().build()).await.unwrap();
}
#[page]
async fn home(cx: &Cx) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<body>
hello(name: "World")
</body>
</html>
})
}
#[component]
async fn hello(cx: &Cx, name: &str) -> Result<impl View> {
Ok(view! { <h1>"Hello, " (name) "!"</h1> })
}What makes Topcoat different.
No. 1
Write browser interactions as type-checked Rust. Topcoat translates them to JavaScript, so a toggle never waits on the server.
No. 2
Mark a component as a shard and it re-renders on the server whenever its inputs change, swapping fresh HTML into the page.
No. 3
The view! macro is plain HTML with Rust's own if, for, and match. The topcoat fmt command formats it for you.
No. 4
Lay out your modules and Topcoat infers the route tree, layouts included, without a build step.
No. 5
Topcoat UI is a Tailwind component library inspired by shadcn/ui. Components are copied into your project, so you can change anything.
No. 6
Declare files with asset! and they are bundled with content-hashed names. Fontsource fonts and Iconify icons are built in.
No. 7
Components load their own data and check their own permissions through the request context. Memoization keeps repeated work cheap.
No. 8
Cookies, sessions, mail, localization, and Tailwind without Node. Plus integrations for htmx, Alpine AJAX, and Datastar.
Examples from the workshop floor.
A $(...) expression is ordinary, type-checked Rust. Topcoat runs it on the server for the first render and translates it to JavaScript, so it re-runs instantly in the browser.
#[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 needs the server, 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>
for product in search_products(cx, &query).await? {
<li>(product.name)</li>
}
</ul>
})
}The view! macro stays true to HTML and to Rust. Loops, conditions, and matches work right inside the markup, and topcoat fmt keeps it tidy.
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>
}Topcoat can work out your routes from your module tree, with no build step. Modules starting with an underscore add a layout without adding to the URL.
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/healthThe bundler finds every asset! call in your compiled app, copies (or downloads) the files, and serves them with content-hashed names so browsers can cache them for good.
const FERRIS: Asset = asset!("./ferris.png");
view! { <img src=(FERRIS)> }Out of the rain in six commands.
http://127.0.0.1:3000 and start editing src/app.rs.cargo install topcoat-clitopcoat new my-app --recommendedcd my-appcargo run -- toasty migration generatecargo run -- toasty migration applytopcoat devRead the full getting started guide, or browse the API documentation.
Planned for future seasons.
Have an idea? Open an issue.
Questions frequently put to the tailor.
Not yet. Topcoat is early-stage and experimental, and breaking changes are expected. It is a fine time to try it and tell the authors what you think.
No. Topcoat renders HTML on the server. Browser interactivity comes from $(...) expressions, which are type-checked Rust that Topcoat also translates to JavaScript.
Axum is a lower-level HTTP router, well suited to API endpoints. Topcoat is aimed at reactive web apps and removes the boilerplate around them. Many projects will use both.
Yes. Topcoat has integrations for htmx, Alpine AJAX, and Datastar if you prefer them to its own browser runtime.
Topcoat's creator hails from Portland, Oregon, where a good coat is not optional. This page, like the city, defaults to drizzle.
In the #topcoat channel on the Tokio Discord, or on GitHub.