Reinhardt Basis Tutorial
Learn the fundamentals of the Reinhardt framework by building a real-world polling application on the reinhardt-pages template — a WASM client, server functions, shared DTOs, an admin panel, and session-cookie authentication.
Overview
This tutorial series walks you through building a fully functional polling application from scratch. The reference implementation lives under examples/examples-tutorial-basis; following the chapters in order will produce a project whose module layout is logically equivalent to it.
Reinhardt's basis tutorial is intentionally different from a classic server-rendered Django-style stack: the UI is a Rust-compiled WebAssembly (WASM) client, the backend exposes typed server functions via #[server_fn], and client and server share the same DTO types. You will see each of these layers introduced explicitly below.
Who This Tutorial Is For
- Developers new to Reinhardt who want to learn the framework from the ground up
- Django developers transitioning to Rust who want to understand Reinhardt's pages architecture
- Anyone building full-stack web applications where the browser runs Rust (WASM) and talks to a server function backend
Prerequisites
- Basic knowledge of Rust programming
- Familiarity with Cargo and
cargo make - Understanding of HTTP concepts and web development
- A code editor or IDE
What You'll Build
A polling application where end users can:
- View the latest polls on a WASM-rendered index page
- Sign up, log in, and create their own polls
- Open a poll, vote on a choice, and see the result update reactively
- See aggregated voting results on a results page
- Edit or delete their own polls (ownership-checked server-side)
Administrators can:
- Create and manage polls via the Reinhardt admin at
/admin/(registered as a server-rendered admin panel with WASM admin assets) - Add, edit, and remove choices for each poll
The Pages Template at a Glance
Every chapter maps onto this layout, which matches the completed example under examples/examples-tutorial-basis/:
examples-tutorial-basis/
├── Cargo.toml # cdylib + rlib; reinhardt with "pages" + "client-router" + "auth-session" features
├── Makefile.toml # cargo make tasks: runserver, migrate, dev, wasm-build-dev, collectstatic, test, …
├── build.rs # cfg_aliases: `native` vs `wasm`
├── index.html # SPA shell with #root mount point and UnoCSS runtime
├── settings/ # TOML settings (base.toml, ci.toml, local.example.toml)
├── src/
│ ├── lib.rs # Entry: declares apps / config / shared / client modules with cfg gates
│ ├── apps.rs # pub mod polls; pub mod users;
│ ├── config.rs # pub mod admin/settings/wasm (cfg native); apps / urls compile both targets
│ ├── shared.rs # pub mod forms (cfg native); pub mod types (both targets)
│ ├── client.rs # pub mod lib / pages / components / links (wasm-only via crate root)
│ ├── bin/
│ │ └── manage.rs # CLI binary (manage.py equivalent), required-features = ["with-reinhardt"]
│ ├── config/
│ │ ├── settings.rs # #[settings(core: CoreSettings)] ProjectSettings + SettingsBuilder + profile loading
│ │ ├── apps.rs # installed_apps! { polls: "polls", users: "users" }
│ │ ├── urls.rs # #[routes(standalone)] routes() -> UnifiedRouter (server_fn registration, admin mount, session middleware)
│ │ ├── wasm.rs # AppStaticFilesConfig for dist-wasm/, registered via inventory::submit!
│ │ └── admin.rs # configure_admin() -> AdminSite + register Question/Choice admins
│ ├── shared/
│ │ ├── types.rs # DTOs: QuestionInfo, ChoiceInfo, VoteRequest, UserInfo, LoginRequest, RegisterRequest
│ │ └── forms.rs # #[cfg(native)] create_vote_form() — server-side Form definition used for unit testing the shape that the client-side form! macro emits (incl. CSRF) at expansion time
│ ├── apps/
│ │ ├── polls/
│ │ │ ├── models.rs # #[model] Question (author FK to User), Choice (question FK)
│ │ │ ├── server_fn.rs # #[server_fn] get_questions / get_question_detail / vote / submit_vote / create_question / …
│ │ │ ├── views.rs # #[get]/#[post] server-rendered JSON endpoints (index, detail, results, vote)
│ │ │ ├── urls.rs # declares urls/server_urls.rs (cfg native) + urls/client_router.rs (cfg wasm)
│ │ │ ├── urls/
│ │ │ │ ├── server_urls.rs # #[url_patterns(InstalledApp::polls, mode = server)] -> ServerRouter
│ │ │ │ └── client_router.rs # #[url_patterns(InstalledApp::polls, mode = client)] -> ClientRouter (named routes)
│ │ │ ├── admin.rs # #[admin(model, for = Question, …)] QuestionAdmin / ChoiceAdmin
│ │ │ └── serializers.rs # QuestionSerializer / ChoiceSerializer with #[validate(...)]
│ │ └── users/
│ │ ├── models.rs # #[user(...)] + #[model] User + project-local UserManager (#[injectable_factory])
│ │ ├── server_fn.rs # #[server_fn] login / register / logout / current_user (session cookie based)
│ │ └── urls/ # server_urls.rs (empty router) + client_router.rs (login / logout / signup pages)
│ └── client/ # WASM-only UI layer (declared in crate root via `pub mod client;` under cfg)
│ ├── lib.rs # #[wasm_bindgen(start)] main(); ClientLauncher::new("#root").register_routes_from_inventory().launch()
│ ├── pages.rs # Page factory functions; wraps body components in with_nav(...)
│ ├── components.rs # pub mod nav; polls; users;
│ ├── components/ # nav.rs / polls.rs / users.rs — page! + watch + form! components
│ └── links.rs # Typed wrappers around ResolvedUrls::resolve_client_url(...) for every named route
└── tests/
├── integration.rs # native; required-features = ["with-reinhardt"]; rstest + serial_test + sqlx + tempfile
└── wasm/polls_mock_test.rs # WASM-only; required-features = ["msw"]; wasm-bindgen-testThree rules keep this structure predictable:
- Native vs WASM —
#[cfg(native)]code runs on the server (models, views, server function bodies, forms, admin).#[cfg(wasm)]code runs in the browser (everything undersrc/client/). Code undersrc/shared/types.rscompiles on both so DTOs stay in sync, and each app declares itsserver_fnandurlsso the typed#[server_fn]client stubs work in the browser. - Server functions are the bridge, and they live per-app — anything the WASM client needs from the database goes through a
#[server_fn]defined insrc/apps/<app>/server_fn.rs(so they sit alongside that app's models, views, and admin), and the result is returned as a DTO fromsrc/shared/types.rs. There is no top-levelsrc/server_fn/directory. - Routing is also per-app, with a typed
urls/directory module — each app exposessrc/apps/<app>/urls/server_urls.rs(#[url_patterns(InstalledApp::<app>, mode = server)] -> ServerRouter) andsrc/apps/<app>/urls/client_router.rs(#[url_patterns(InstalledApp::<app>, mode = client)] -> ClientRouter). The framework auto-mounts them by inventory using the typedInstalledApp::<app>identifier; the project-levelsrc/config/urls.rsonly registers#[server_fn]entries, mounts/admin/, and applies middleware.
Tutorial Structure
Part 1: Project Setup
- Install
reinhardt-admin-cliand generate a project from thepagestemplate - Walk the
src/{lib,apps,config,shared,client,bin}layout the template emits - Configure
settings/base.tomland load it through theProjectSettings+SettingsBuilderpipeline (note:TomlFileSourceinterpolation is enabled by default) - Run the dev server with
cargo make runserver(auto-runsmigratefirst) and the full WASM workflow withcargo make dev
Part 2: Models and Database
- Define
QuestionandChoiceundersrc/apps/polls/models.rswith#[model(app_label = "polls", table_name = "...")], using#[field(...)]and#[rel(foreign_key, related_name = "...")]to wireQuestion.author -> UserandChoice.question -> Question - Introduce the
usersapp and theUsermodel defined with#[user(hasher = Argon2Hasher, username_field = "username", manager = false)] + #[model(...)], plus a project-localUserManagerregistered via#[injectable_factory(scope = "transient")] - Register both apps in
src/config/apps.rsviainstalled_apps! { polls: "polls", users: "users" } - Generate and apply migrations with
cargo make makemigrationsandcargo make migrate
Part 3: Server Functions, Views, and URLs
- Write server functions under
src/apps/polls/server_fn.rsandsrc/apps/users/server_fn.rs— this is the "views" layer for the WASM client - Write server-rendered HTTP endpoints under
src/apps/polls/views.rsfor clients that want a plain JSON API - Split routing into
src/apps/<app>/urls/server_urls.rs(ServerRouter) andsrc/apps/<app>/urls/client_router.rs(ClientRouter), both registered via#[url_patterns(InstalledApp::<app>, mode = ...)] - Register server functions in
src/config/urls.rswithUnifiedRouter::new().server(|s| s.server_fn(name::marker)...)— app routers are mounted automatically - Bootstrap the SPA in
src/client/lib.rswithClientLauncher::new("#root").register_routes_from_inventory().launch(); the#[routes(standalone, client_inventory)]aggregator insrc/config/urls.rscomposes each app's client router viaUnifiedRouter::mount_unifiedand submits the result intoinventory, which the launcher then collects (PR #4453)
Part 4: Forms and Generic Views
- Define
create_vote_form()insrc/shared/forms.rs(server-only, behind#[cfg(native)]) usingForm::new().add_field(CharField::new(...).with_widget(Widget::HiddenInput)) - Let the client-side
form!macro emit the matchingFormMetadata(incl. CSRF token) at expansion time — thestrip_arguments: { csrf_token: ::reinhardt::reinhardt_pages::csrf::get_csrf_token().unwrap_or_default() }clause forwards the per-request token to the trailing server-fn parameter - Build the voting UI in
src/client/components/polls.rswith theform!macro +watch { ... }blocks inside apage!component - Call
submit_vote(a#[server_fn]incrate::apps::polls::server_fn) on submit; show server validation errors reactively
Part 5: Testing
- Use
rstestfixtures +reinhardt-testhelpers +sqlx+tempfile(all under[target.'cfg(not(...))'.dev-dependencies]) to spin up an isolated SQLite for native integration tests - Mark the native integration target with
[[test]] name = "integration", required-features = ["with-reinhardt"] - Add a WASM-only target at
tests/wasm/polls_mock_test.rs(#![cfg(wasm)],required-features = ["msw"]) that mocks server function HTTP calls via MSW - Follow the Arrange-Act-Assert pattern with
// Arrange,// Act,// Assertlabels
Part 6: Static Files
- Understand the two static-asset tiers used by the pages template:
dist-wasm/— output ofcargo make wasm-build-dev/wasm-build-release, registered viaAppStaticFilesConfig+inventory::submit!insrc/config/wasm.rsstaticfiles/— final output ofcargo make collectstatic, served at/static/
- Wire it all up through
Makefile.tomltasks (runserver,dev,wasm-build-dev,collectstatic,dev-release)
Part 7: Admin Customization
- Register
ModelAdminimplementations app-side with#[admin(model, for = ..., ...)]insrc/apps/polls/admin.rs - Compose the project-wide
AdminSiteinsrc/config/admin.rsand mount it at/admin/fromsrc/config/urls.rsviaadmin_routes_with_di - Customize list columns, search fields, filters, ordering, and per-page limits
Recommended Learning Path
Work through the chapters in order. Each chapter assumes the directory layout produced by the previous one. If you get stuck, compare your tree against examples/examples-tutorial-basis/ — the reference source is the authoritative answer key.
Getting Help
Comparison with REST Tutorial
If you're also interested in building pure JSON APIs, see the REST Tutorial.
- Basis Tutorial (this one): full-stack pages template — WASM client +
#[server_fn]+ shared DTOs + admin + session auth. - REST Tutorial:
#[get]/#[post]views,Serializers, andViewSet+Routerfor classic REST endpoints.
The underlying model and database layers are identical, so lessons transfer in both directions.
Let's Get Started!
Head over to Part 1: Project Setup to generate your first pages project.