Part 1: Project Setup
Part 1: Project Setup
In this tutorial, we'll install the Reinhardt CLI, generate a new project from the pages template, walk through the layout it emits, and run it locally. By the end of the chapter your tree will be logically equivalent to examples/examples-tutorial-basis; the chapters that follow add models, server functions, forms, tests, and admin customization on top of this scaffold.
Verifying Your Installation
Before we begin, let's verify that Rust and Cargo are installed correctly:
rustc --version
cargo --versionYou should see version information for both commands. If not, visit rust-lang.org to install Rust.
The tutorial also assumes you have cargo-make installed for the cargo make … task runner:
cargo install cargo-makeInstalling Reinhardt Admin CLI
Install the global tool for project generation. While Reinhardt is on a pre-release (-rc.* / -alpha.*), cargo install requires an explicit --version because pre-releases are not selected by default. Once 0.1.0 stable ships, omit --version to pull the latest stable (or keep --version as an opt-in reproducibility pin). The literal below is auto-bumped by release-plz on each release.
cargo install reinhardt-admin-cli --version "0.1.2"Creating a Project
This tutorial uses the reinhardt-pages template — a WASM client + server functions + shared types layout. Generate the project from that template:
reinhardt-admin startproject polls_project --template pages
cd polls_projectThe generated tree matches the reference implementation in examples/examples-tutorial-basis/:
polls_project/
├── Cargo.toml # cdylib + rlib; reinhardt server target with "full" + "pages" + "conf" + "commands" + "db-sqlite" + "forms" + "client-router" + "auth-session"; WASM target with "pages" + "client-router"
├── Makefile.toml # cargo make runserver / migrate / dev / wasm-build-dev / collectstatic / test / …
├── build.rs # cfg_aliases: `native` vs `wasm`; also emits `with_reinhardt` cfg after auto-detecting the parent workspace
├── index.html # SPA shell — single #root mount + UnoCSS runtime
├── README.md
├── favicon.png
├── migrations/ # Generated by `cargo make makemigrations`
├── scripts/ # Helper shell scripts for wasm-build-*, run-dev-server, clean-cache
├── static/ # Project-level static assets (css/, images/, …)
├── settings/
│ ├── base.toml # always loaded
│ ├── ci.toml # loaded when REINHARDT_ENV=ci or CI is set
│ └── local.example.toml # copy to local.toml for local-only overrides
├── src/
│ ├── lib.rs # Crate root; 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 (gated to cfg wasm at the crate root)
│ ├── bin/
│ │ └── manage.rs # CLI binary (Django's 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, client_inventory)] routes() -> UnifiedRouter (server_fn registration, admin mount, session middleware, client-router aggregation)
│ │ ├── wasm.rs # AppStaticFilesConfig for dist-wasm/, registered via inventory::submit!
│ │ └── admin.rs # configure_admin() -> AdminSite + register Question/Choice admins
│ ├── shared/
│ │ ├── types.rs # Shared DTOs (filled in Part 2 onward)
│ │ └── forms.rs # Server-only Form definitions (filled in Part 4)
│ ├── apps/
│ │ ├── polls/ # Filled in Part 2 onward (models, server_fn, views, urls/, admin, serializers)
│ │ └── users/ # Filled in Part 2 (User model + auth server functions)
│ └── client/ # Filled in Part 3 (lib.rs entry, pages.rs, components/, links.rs)
│ └── …
└── tests/
├── integration.rs # Native integration tests, required-features = ["with-reinhardt"]
└── wasm/
└── polls_mock_test.rs # WASM mock tests, required-features = ["msw"] (#![cfg(wasm)]-gated)Three rules keep this layout predictable:
#[cfg(native)]vs#[cfg(wasm)]— server-only code (models, views, server function bodies, forms, admin) is gated onnative; browser-only code (everything insrc/client/) is gated onwasm.src/shared/types.rscompiles on both so DTOs stay in sync, and each app'sserver_fnandurlsmodules are both targets so the typed client stubs work in the browser.- Server functions are the bridge, and they live per-app — every
#[server_fn]lives insrc/apps/<app>/server_fn.rs, sitting next to that app's models, views, and admin. There is no top-levelsrc/server_fn/directory. - Routing is per-app, with a typed
urls/directory module —src/apps/<app>/urls.rsdeclarespub mod server_urls;(#[cfg(native)]) andpub mod client_router;(#[cfg(wasm)]). The framework auto-mounts them viainventorybecause each function carries#[url_patterns(InstalledApp::<app>, mode = server|client|unified)].
Available cargo make tasks (defined in Makefile.toml):
| Task | Purpose |
|---|---|
cargo make runserver | Run the dev server (depends on migrate); equivalent to cargo run --bin manage runserver --with-pages |
cargo make dev | One-shot: clean-cache → wasm-build-dev → run-dev-server (the most common command during a tutorial session) |
cargo make makemigrations | Generate migrations from model changes |
cargo make migrate | Apply migrations to the configured database |
cargo make wasm-build-dev | Compile the WASM bundle (debug) and emit it under dist-wasm/ |
cargo make wasm-build-release | Compile + optimise (wasm-opt) the WASM bundle for release |
cargo make collectstatic | Collect static assets (including dist-wasm/) into staticfiles/ |
cargo make test | cargo nextest run --all-features |
cargo make wasm-test | Run WASM tests under wasm-pack test --headless --chrome (passes --no-default-features so the manage binary is skipped) |
cargo make showurls | Print every registered URL pattern |
cargo make check | Project self-check (Django-style check) |
cargo make fmt-check / fmt-fix | reinhardt-admin fmt for the page! DSL + rustfmt |
cargo make clippy-check / clippy-fix | Clippy with -D warnings |
cargo make quality / quality-fix | Run both fmt-* and clippy-* |
Note: This tutorial targets the reinhardt-pages architecture end-to-end. If you are instead building a pure JSON backend consumed by an external SPA or mobile client, start with the REST Tutorial.
Understanding the Project Structure
Each generated file has a specific role. Walking top-down:
Cargo.toml— declarescrate-type = ["cdylib", "rlib"](cdylib for WASM, rlib for the server binary),default-run = "manage", and an[[bin]] managewhoserequired-features = ["with-reinhardt"]keep tokio + reinhardt-commands out of the WASM build. It splits dependencies between two[target.'cfg(...)'.dependencies]blocks: the server side enablesreinhardtwithfull + pages + conf + commands + db-sqlite + forms + client-router + auth-session, while the WASM side only enablespages + client-router. Two test targets are declared explicitly:[[test]] name = "integration", required-features = ["with-reinhardt"](native) and[[test]] name = "polls_mock_test", path = "tests/wasm/polls_mock_test.rs", required-features = ["msw"](WASM). The crate-local[features]block addswith-reinhardt(native gate),client-router, andmsw(forwarded to the facade so#[server_fn]emitsMockableServerFnmarkers).build.rs— uses thecfg_aliasescrate to register two custom cfgs:wasm=all(target_family = "wasm", target_os = "unknown")andnative= its negation. You will see these throughout the source as#[cfg(wasm)]/#[cfg(native)]. The build script also auto-detects whether the parent directory is thereinhardt-webworkspace (subtree development) versus a standalone checkout, and unconditionally emitscargo:rustc-cfg=with_reinhardtin both modes so the integration test target compiles either way.index.html— the SPA shell. It loads UnoCSS from a CDN, defines a#rootdiv that the launcher mounts into, and shows aLoading…spinner while the WASM bundle downloads.settings/— TOML settings files.base.tomlis always loaded;{profile}.toml(resolved fromREINHARDT_ENV, orciwhen theCIenv var is set, orlocalotherwise) layers on top.local.example.tomlis a template — copy it tosettings/local.tomlfor local-only overrides.src/lib.rs— the crate root. It declarespub mod apps;,pub mod config;,pub mod shared;, and#[cfg(wasm)] pub mod client;. Server-only re-exports (async_trait, thereinhardt_apps/reinhardt_core/reinhardt_di::params/reinhardt_httpshims) are gated on#[cfg(native)].src/bin/manage.rs— the server-side binary. It setsREINHARDT_SETTINGS_MODULE = "examples_tutorial_basis.config.settings"(rename to your crate name in the generated tree) and callsreinhardt::commands::execute_from_command_line(). The WASM build still needs amainsymbol forbincrate-types, so the file also defines an emptyfn main() {}under#[cfg(target_arch = "wasm32")].src/config/settings.rs—#[settings(core: CoreSettings)] pub struct ProjectSettings;plus aget_settings()function that builds the layeredSettingsBuilder(DefaultSource→LowPriorityEnvSource("REINHARDT_")→TomlFileSource("base.toml")→TomlFileSource("{profile}.toml")). Profile resolution lives in a privateprofile_name()helper.apps.rs—installed_apps! { polls: "polls", users: "users" }. The macro generates theInstalledAppenum used as the typed argument to#[url_patterns(InstalledApp::<app>, mode = ...)].urls.rs—#[routes(standalone, client_inventory)] pub fn routes() -> UnifiedRouter. Registers every server function via.server(|s| s.server_fn(name::marker)), mounts the admin at/admin/(plus/static/admin/) viaadmin_routes_with_di(Arc::new(configure_admin())), and applies the session middleware. Theclient_inventoryflag (#4453) drops the native-only cfg gate from the function body and emitsinventory::submit!(ClientRouterRegistration)onwasm32-unknown-unknown; a#[cfg(wasm)]block aggregates each app'sclient_url_patterns()into the sameUnifiedRouterso the SPA route table is complete. Server-side per-app routers are still discovered through their own#[url_patterns(InstalledApp::<app>, mode = server)]registrations, sosrc/config/urls.rsdoes not need explicit.mount("/polls/", ...)calls.wasm.rs— aninventory::submit!entry that registersdist-wasm/as anAppStaticFilesConfig, socargo make collectstaticdiscovers the WASM build artifacts and copies them intostaticfiles/.admin.rs—configure_admin() -> AdminSiteinstantiates the admin site, names it, and registers each app'sModelAdminimplementations (QuestionAdmin,ChoiceAdmin).
src/shared/types.rs— DTOs (QuestionInfo,ChoiceInfo,UserInfo,LoginRequest,RegisterRequest,VoteRequest) shared between WASM and server.Validatederives are wrapped in#[cfg_attr(native, derive(Validate))]so the WASM client does not pull in the validator crate.forms.rs—#[cfg(native)]-onlyFormdefinitions used by theform!macro on the client (forms are constructed server-side and serialized toFormMetadata).
src/apps/— Reinhardt apps. Each app owns its models, server functions, views, URLs, admin, and serializers. We fill these in starting from Part 2.src/client/— WASM-only UI.lib.rsis the#[wasm_bindgen(start)]entry that callsClientLauncher::new("#root").register_routes_from_inventory().launch(), picking up everyClientRouterRegistrationthat the#[routes(standalone, client_inventory)]aggregator insrc/config/urls.rssubmitted toinventory(PR #4453).pages.rsexposes page factories,components/contains thepage!components, andlinks.rswrapsResolvedUrls::resolve_client_url(...)so components never construct URLs by hand. We build this layer in Part 3.
Architecture: WASM + SSR (reinhardt-pages)
This tutorial uses the WASM + SSR architecture with reinhardt-pages, ideal for:
- Full-stack web applications with an integrated frontend and backend
- Single Page Applications (SPAs) with server-side rendering
- Type-safe client-server communication
- Modern reactive user interfaces
The data flow for one user interaction looks like this:
flowchart LR
Browser["Browser<br/>(loads WASM)"] -->|Router matches URL| Component["page! component<br/>(src/client/components/…)"]
Component -->|"calls #[server_fn] stub"| ServerFn["server function<br/>(src/apps/<app>/server_fn.rs)"]
ServerFn -->|DI: DatabaseConnection,<br/>SessionData, …| Model["Model<br/>(src/apps/<app>/models.rs)"]
Model -->|via reinhardt-query| DB[(Database)]
DB --> Model
Model --> ServerFn
ServerFn -->|"Result<DTO, ServerFnError>"| Component
Component -->|"watch { … } re-renders"| BrowserKey characteristics:
- Unified codebase for frontend and backend
- Type-safe RPC-style communication via
#[server_fn] - Client-side reactivity (
page!+watch+use_action) - Single deployment artifact
- WASM compilation for the client-side UI
Alternative: RESTful API architecture. If you're building a backend API for separate frontends (React, Vue, mobile apps), see the REST API Tutorial instead.
Configuring settings/base.toml
settings/base.toml holds the always-loaded base layer of your settings. Open it and confirm it contains at least the keys consumed by the [core] and [database] fragments:
[core]
debug = false
secret_key = "CHANGE_THIS_IN_PRODUCTION"
allowed_hosts = []
installed_apps = []
middleware = []
root_urlconf = ""
[core.security]
secure_ssl_redirect = false
secure_hsts_include_subdomains = false
secure_hsts_preload = false
session_cookie_secure = false
csrf_cookie_secure = false
append_slash = true
[database]
engine = "sqlite"
name = "db.sqlite3"A few things worth knowing as you edit:
TomlFileSource::new(path)applies${VAR}interpolation by default (changed in 0.1.0-rc.27). If you want a literal${...}to survive the load, opt out per file with.without_interpolation(). The deprecatedwith_interpolation(bool)setter still works in 0.1.x but will be removed in 0.2.0.- The JSON file source (
JsonFileSource,auto_source) is deprecated and will be removed in 0.2.0. Stick to TOML. - For local-only overrides (e.g., a real
DATABASE_URL), copysettings/local.example.tomltosettings/local.tomland edit there — it is gitignored.
settings/{profile}.toml is selected dynamically. The resolution order is:
$REINHARDT_ENVif set (e.g.,staging,production).- Else
ciwhen$CIis set (matches GitHub Actions / CircleCI). - Else
local.
Look in src/config/settings.rs to see this wired up:
use reinhardt::conf::settings::builder::SettingsBuilder;
use reinhardt::conf::settings::profile::Profile;
use reinhardt::conf::settings::sources::{DefaultSource, LowPriorityEnvSource, TomlFileSource};
use reinhardt::core::serde::json;
use reinhardt::settings;
use std::env;
use std::path::PathBuf;
#[settings(core: CoreSettings)]
pub struct ProjectSettings;
fn profile_name() -> String {
env::var("REINHARDT_ENV").unwrap_or_else(|_| {
if env::var("CI").is_ok() {
"ci".to_string()
} else {
"local".to_string()
}
})
}
fn resolve_settings_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("settings")
}
pub fn get_settings() -> ProjectSettings {
let profile_str = profile_name();
let settings_dir = resolve_settings_dir();
let base_dir = env::current_dir().expect("Failed to get current directory");
SettingsBuilder::new()
.profile(Profile::parse(&profile_str))
.add_source(DefaultSource::new().with_value(
"core.base_dir",
json::Value::String(base_dir.to_string_lossy().to_string()),
))
.add_source(LowPriorityEnvSource::new().with_prefix("REINHARDT_"))
.add_source(TomlFileSource::new(settings_dir.join("base.toml")))
.add_source(TomlFileSource::new(
settings_dir.join(format!("{}.toml", profile_str)),
))
.build_composed()
.expect("Failed to build settings")
}Need a project-specific setting beyond
CoreSettings? You can compose fragments with|, e.g.,#[settings(CoreSettings | AuthSettings | DatabaseSettings)] pub struct ProjectSettings;. The basis tutorial keeps things minimal with justCoreSettings; we addDatabaseSettings-driven access in Part 2.
Configuring installed_apps!
src/config/apps.rs declares which Reinhardt apps the framework should discover:
use reinhardt::installed_apps;
installed_apps! {
polls: "polls",
users: "users",
}
pub fn get_installed_apps() -> Vec<String> {
InstalledApp::all_apps()
}The installed_apps! macro generates the InstalledApp enum that the rest of the codebase uses as the typed argument to #[url_patterns(InstalledApp::<app>, mode = ...)]. Two consequences worth knowing:
- The left-hand side (
polls:) is the enum variant (InstalledApp::polls). - The right-hand side (
"polls") is the app label that ends up in the URL prefix the framework auto-applies to each app's routers (/polls/here).
Reinhardt's framework features (auth, sessions, admin, REST, etc.) are not registered through installed_apps!; they are enabled through Cargo feature flags. See the Feature Flags Guide for the full mapping.
Running the Development Server
You have two choices for running the dev server locally.
cargo make runserver — server only
Use this when you have already built the WASM bundle (or do not need it):
cargo make runserverrunserver depends on migrate, so it will create the SQLite file on first run. Internally it executes cargo run --bin manage runserver --with-pages, which starts the server on http://127.0.0.1:8000/ and serves the SPA shell at /.
cargo make dev — WASM build + dev server
This is the most common command during the tutorial. It cleans the WASM cache, rebuilds the bundle in debug mode, and starts the dev server:
cargo make devYou should see output similar to:
Compiling polls_project v0.1.0 (/path/to/polls_project)
Finished dev [unoptimized + debuginfo] target(s) in 2.34s
Running `target/debug/manage runserver --with-pages`
Reinhardt Development Server
──────────────────────────────────────────────────
✓ http://127.0.0.1:8000
Environment: Debug
Quit the server with CTRL+COpen your web browser and visit http://127.0.0.1:8000/. You will see the SPA shell from index.html mount the WASM bundle and (once Part 3 lands) display the polls index page.
For a production-grade build, the dev-release family of tasks does the same orchestration but invokes wasm-build-release (with wasm-opt) and collectstatic:
cargo make dev-releaseUnderstanding What Happened
The pages template gave you a five-section project:
- Server-side modules (
src/apps/*/models.rs,views.rs,server_fn.rs,urls/server_urls.rs,admin.rs,serializers.rs) — gated on#[cfg(native)]so they vanish from the WASM build. - WASM-side modules (
src/client/*,src/apps/*/urls/client_router.rs) — gated on#[cfg(wasm)]. - Shared modules (
src/shared/types.rs, each app'sserver_fn.rssignatures) — compile for both targets so DTOs and typed RPC stubs stay in sync. - Project-level glue (
src/config/*,src/bin/manage.rs) — the entry points the framework looks up:routes(),get_installed_apps(),get_settings(),configure_admin(),execute_from_command_line(). - Task automation (
Makefile.toml) — every command you will use throughout the tutorial, fromcargo make migratetocargo make wasm-build-release.
What's Next?
We've created a working pages project with URL routing scaffolding, settings, the management CLI, and the SPA shell. In the next chapter we add the Question, Choice, and User models, wire them through migrations, and prepare the database for the rest of the tutorial.
When you're ready, move on to Part 2: Models and Database.
Summary
In this tutorial you learned:
- How to install
reinhardt-admin-cliand generate a project from thepagestemplate - How the
src/{lib, apps, config, shared, client, bin}layout maps to native vs WASM code paths viacfg_aliases - How
settings/base.toml+ profile overlays are loaded throughSettingsBuilder(with${VAR}interpolation enabled by default) - How
installed_apps!exposesInstalledApp::polls/InstalledApp::usersto the typed#[url_patterns]attribute - How
cargo make runserverandcargo make devdiffer — and whichcargo maketasks ship inMakefile.toml - The reinhardt-pages data flow: page component →
#[server_fn]stub → model → database → DTO → reactive re-render