Originally published at kunalganglani.com — read it there for inline code, hero image, and live links.
If you want a Next.js-style “full-stack” flow, but you’re allergic to the JS toolchain tax, this topcoat rust web framework tutorial is the walkthrough I wish existed.
The one prerequisite that trips people up is not Rust. It’s picking your persistence + migration story on day 1, because everything else (auth, forms, deploy) stacks on top of it. I’m going to build a small app end-to-end, then call out the production gotchas you’ll hit the moment you put it behind a reverse proxy.
Also: my own keyword research pipeline found basically no indexed written tutorials for this exact query, and only tiny “neighborhood” demand (about 9.9 searches/month across related queries). That’s not a reason to skip it. It’s a reason to write the first tutorial that doesn’t hand-wave auth, migrations, and SSR performance.
Here’s the official-ish vibe check if you want a video companion. Francesco Ciulla’s hands-on attempt frames Topcoat explicitly as a Next.js contender: Francesco Ciulla.
What is Topcoat?
Topcoat is a Rust full‑stack web framework that aims to give you a single, integrated workflow for routes, server logic, and server-side rendering (SSR) without needing a separate JavaScript framework and build pipeline.
The promise is ergonomic: build UI + server actions in one project, ship a single deployable, and still get modern UX patterns (forms, mutations, protected pages) with an SSR-first mental model.
I’m going to treat Topcoat the way I treat any framework that claims “full-stack”: if it can’t handle auth, migrations, and deploys cleanly, it’s not full-stack. It’s a demo generator.
Topcoat Rust web framework tutorial: build a full-stack app (auth + DB + SSR)
[YOUTUBE:ppp4A2B0FeA|Better than Next.js? I Tried Rust’s New Full-Stack Web Framework: Topcoat]
I’ll outline the steps first, then go deeper where the footguns live.
- Install Rust toolchain and create a new Topcoat app
- Understand project structure: routes, server actions, SSR
- Add a database (Postgres by default), wire up env vars, add a pool
- Add migrations and make them safe for CI/CD and prod rollbacks
- Implement auth: signup/login/logout, hashed passwords, secure session cookies
- Build forms with validation + CSRF strategy
- Profile SSR performance and fix the first real bottlenecks
- Deploy (Docker or native), handle static assets, secrets, and DB provisioning
Step 1: Create a new Topcoat app and run it locally
Because I couldn’t reliably pull Topcoat’s exact CLI commands from search in my environment, I’m going to describe the workflow in a way that stays stable even if the CLI naming shifts.
- Install Rust (
rustup) and pick a stable toolchain. - Scaffold a new app using Topcoat’s generator (or clone the starter template).
- Run the dev server.
The core thing to check in the first 5 minutes:
- Do you get a live-reload or “hot reload” story, or is it “recompile + refresh”?
- Is the router file-based or explicit?
- Where do server actions live?
If you’ve worked with Rust long enough, you already know the trade: great runtime performance, and you pay with compile time. If compile times annoy you now, fix it early. I use the same tricks I wrote about in [How to Reduce Rust Compile Time 2026](/blog/reduce-rust-compile-time).
Step 2: Routes, server functions/actions, and SSR (the mental model)
Topcoat’s “full-stack” value is basically three primitives:
- Routes: the URL-to-component mapping.
- Server actions/functions: the mutation/read endpoints that the UI can call.
- SSR: the initial HTML render happens server-side, then (optionally) hydrates.
The production question to ask: what’s the boundary between “UI code” and “server code”? In Rust frameworks, it’s easy to accidentally couple these so tightly that refactors become “rename 12 modules and fix 60 compiler errors”. You want a boundary that matches your domain.
My stance: treat server actions like a real API surface. Version it mentally. Validate inputs. Log it. Put auth checks there, not sprinkled around components.
This is the same engineering instinct that prevents painful rewrites. I’ve written about that bias explicitly in Software Rewrite from Scratch: Why It’s Almost Always the Worst Engineering Decision [2026].
Step 3: Add a database (Postgres vs SQLite), pooling, env vars
You can build this tutorial app with either:
- SQLite: dead-simple local dev, great for single-node deploys.
- Postgres: better concurrency and operational tooling once you grow.
If you don’t already have a strong reason for SQLite-in-prod, default to Postgres. The moment you add “real auth”, you’ll want proper connection pooling and visibility.
What I do in practice:
- Use
DATABASE_URLas the single source of truth. - In dev, set it via
direnv(or a.envfile you never commit). - In prod, set it via your platform secret manager.
If you want the tradeoffs written out, I already covered the decision points in SQLite vs PostgreSQL 2026: Which DB Wins for App Backends?.
Concrete numbers matter here. A “pool size of 5–10” is often enough for small apps, but the right number depends on your Postgres plan limits and your SSR concurrency. Start at 5, measure, then increase.
Step 4: DB migrations that don’t ruin your Friday night
Migrations are where “tutorial apps” go to die.
You need:
- A migration tool (Rust-native or external) that runs in CI
- A policy for forward-only vs rollbacks
- A way to test migrations on a clean DB
My production bias is forward-only migrations with explicit “down” only for development. Rollbacks are often fantasy because data shape changes.
Minimum CI checks I’d add on day 1:
- On every PR: spin up a fresh Postgres, run all migrations from zero.
- On main: run migrations in a staging environment before production.
This connects directly to how I think about CI systems and repeatability. If your pipeline is flaky, everything else is pointless. See GitHub Actions vs CircleCI 2026: Which CI/CD Pipeline Wins?.
And for secret handling around DB URLs in CI, I use the same playbook as How to Set Up gitleaks + pre-commit + CI [2026] plus a hard “no secrets in logs” rule.
Auth end-to-end: signup/login/logout, hashing, cookies, protected routes
This is the section most Rust framework tutorials avoid because it’s where people get hacked.
Here’s the shape that works:
-
userstable (id, email, password_hash, created_at) -
sessionstable (id, user_id, created_at, expires_at, revoked_at) - Session cookie stores a random session id (not a JWT you can’t revoke)
Password hashing
Don’t invent crypto. Pick a well-worn algorithm.
- Use Argon2id.
- Use a per-user random salt.
- Tune parameters so hashing takes ~50–200ms on your server hardware.
If you’ve shipped large systems, you learn this the boring way. Password hashing that is “too fast” is an attacker’s best friend. Password hashing that’s “too slow” becomes a self-inflicted DoS.
Session cookies
Cookie defaults will quietly betray you.
-
HttpOnly=true(prevents JS access) -
Secure=truein production (HTTPS only) -
SameSite=Laxas a default baseline for typical app flows - Set a reasonable TTL like 7–30 days, and rotate on login
When you deploy behind a proxy, the first production bug is always “Secure cookies not set because the app thinks it’s on HTTP”. Make sure Topcoat (and your reverse proxy) agree on forwarded headers.
If you want a deeper “proxy correctness” rabbit hole, I wrote about reverse-proxy setups in How to Run Anubis WASM Bot Filter as a Reverse Proxy [2026].
Protecting routes
Don’t hide pages in the UI. Enforce auth on the server.
- On SSR render of protected routes: check session -> user.
- On server actions: re-check session -> user.
Yes, it’s duplicate work. Yes, it’s correct.
Forms: validation, errors, and CSRF strategy
Forms are where full-stack frameworks either feel magical or feel like 2012.
A production-grade pattern:
- Server action receives typed input
- Validate on server
- Return field errors (per input) + a general error
- Re-render SSR with errors inline
Validation rules should be explicit and numeric. Example:
- Email max length 320
- Password min length 12
- Rate limit login attempts to 5 per minute per IP (or per account)
CSRF
I’m opinionated here: use SameSite cookies as a baseline, but still add CSRF tokens on state-changing form posts if your app has any non-trivial risk profile.
Why? SameSite helps, but it’s not a universal shield. If you later add cross-site flows (OAuth, embedded widgets, subdomains), you’ll re-open the problem.
Also: most people only discover CSRF after they add “Delete account” and realize it’s one POST away from chaos.
If you care about threat modeling as a habit, this overlaps with how I think about AI security and specifically how “defaults” create silent exposure.
SSR performance profiling: what to measure, traces/flamegraphs, bottlenecks
SSR performance is where the “Rust should be fast” myth meets reality.
SSR is not slow because Rust is slow. SSR is slow because you:
- Block on database calls
- Do N+1 queries
- Render too much per request
- Serialize/deserialize too much data
What I measure first:
- P50, P95, P99 request latency (ms)
- DB query count per request (target: < 10 for typical pages)
- Time spent in template/render (ms)
Instrumentation approach:
- Add structured logs with a request id.
- Add tracing spans around: SSR render, DB queries, auth lookup.
I use the same mental model I use for agent systems: you need an execution trace you can read. That’s why I built an “trace tree” approach in Execution Trace Tree for AI Agents: Build One in 60 Minutes. The tooling differs, but the debugging philosophy is identical.
If you run into the classic bottleneck where SSR is fast locally but slow in prod, check:
- TLS termination (proxy)
- CPU throttling on cheap instances
- DB network latency (same region?)
- Connection pool starvation (pool size too small)
A concrete example: if your DB is 20ms away and you do 25 sequential queries, you just bought yourself 500ms of latency before rendering even starts.
Deploying a Topcoat app: Docker vs native, assets, secrets, DB provisioning
Deploy is where framework marketing dies.
Docker vs native builds
- Docker: easiest reproducibility and environment parity. Slower builds unless you cache aggressively.
- Native: smaller surface area at runtime, but you must control libc compatibility and OS packages.
If you’re new to production containers, also read How to Secure Docker Rootless Mode in Production [2026]. Even small apps become big targets.
Static assets
Pick one:
- Bake assets into the container/image.
- Or serve assets via a CDN/object storage.
For most small deployments, baking into the image is fine. When you grow, offload static assets.
Secrets and env vars
Do not ship secrets in .env.
- Local dev:
.env+.gitignore - Team dev:
direnv+ per-dev secrets - Prod: platform secrets (Fly.io, Render, Railway, etc.)
If you’ve ever leaked a key through shell history, you stop messing around. I have a specific checklist for that in Prevent API Key Leaks in Shell History (bash/zsh/fish) [2026].
DB provisioning and backups
If you pick Postgres, you need a backup story before you need a backup.
I’m blunt about this: a migration without backups is just gambling.
For Postgres backup mechanics and restore testing, use my playbook: How to Back Up PostgreSQL With pgBackRest [S3 + Restore Test].
Production gotchas: timeouts, proxies, background jobs, observability
These are the things that break “full-stack” frameworks in real deployments.
Timeouts
- Put an explicit request timeout (e.g. 10–30s) at the proxy.
- Put a shorter timeout (e.g. 3–5s) for SSR DB calls.
If SSR can hang forever, it will. Under load, this becomes a self-amplifying failure.
Reverse proxy headers
Make sure you handle:
-
X-Forwarded-Proto(so Secure cookies work) -
X-Forwarded-For(so rate limiting/logging uses real IP)
If you don’t, you’ll rate-limit your proxy and let attackers through.
Background jobs
If you need emails, cleanup tasks, or webhooks, don’t jam it into the request path.
- Use a job runner or separate worker process.
- Store job state in the DB.
This is the same principle I learned building workflow-y systems. At Swiggy, while building cancellation and refund workflows at “millions of deliveries” scale, I learned that workflow microservices need explicit compensation paths, not retries. That lesson applies here too. Don’t “retry” side effects and hope.
Observability
Minimum viable setup:
- Structured logs (JSON)
- Request id propagated end-to-end
- A few RED metrics: rate, errors, duration
If you already run OpenTelemetry elsewhere, you’ll want parity. My AI observability stack is in How to Build Vendor-Neutral LLM Observability Monitoring [2026]. Different domain, same mechanics.
Topcoat vs Axum+Leptos vs Next.js: when you should not use it
Here’s the honest take.
- Topcoat: you want an integrated Rust “app” framework. You accept some API churn to get speed of shipping.
- Axum + Leptos/Dioxus/Yew: you want to assemble your own stack. You’ll write more glue, but you’ll control the seams.
- Next.js/Remix: you want the biggest ecosystem, integrations, and hiring pool. You pay the JS tax.
When you should not use Topcoat:
- Your team needs a frozen API surface for 2+ years.
- You’re going to need a huge plugin ecosystem on day 1.
- Your app is mostly an API with a small admin UI. Use Axum and keep it boring.
If you’re deciding between server-centric HTML and React-style complexity, I’d also read Hotwire vs Next.js in 2026: Is Server-Centric HTML the End of SPA Bloat? [Compared].
The prediction I’ll make: if Topcoat can stabilize its migration + auth + deploy story, Rust’s web ecosystem will finally have a “default” full-stack option. If it can’t, Topcoat will still be valuable. It will become the framework that teaches the rest of the ecosystem what “full-stack Rust” should feel like.
Originally published on kunalganglani.com