penguin_nurse is a full-stack Rust web application built with Dioxus 0.7 (React-like
UI framework), Axum 0.8 (HTTP server), Diesel 2.2 (async PostgreSQL ORM), and
axum-login (OIDC-based authentication). The frontend compiles to WASM; the backend runs
on the server. CSS is Tailwind v4 + DaisyUI v5; a small barcode-scanner JS bundle is built
with Rollup. The build environment is managed via Nix flakes and devenv.
All commands assume you are inside the Nix devenv shell (devenv shell or nix develop).
dx serve --platform webnpx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch./node_modules/.bin/rollup --config rollup.config.mjscargo build --bin lint --features cli-only
# or in release mode:
cargo build --bin lint --features cli-only --releasenix build .#default # fullstack web app
nix build .#lint # lint binary onlycargo check # fast type check (all features)
cargo check --features cli-only # check the lint binary specifically
cargo clippy # lints; fix warnings before committing
cargo fmt # auto-format (rustfmt, Rust 2024 edition)
cargo fmt --check # verify formatting without writing changesThere is no custom rustfmt.toml; default rustfmt settings apply.
There are no unit or integration tests in Rust source. The [dev-dependencies]
section in Cargo.toml is intentionally empty.
The only automated test is a NixOS integration/smoke test that:
- Starts the full
penguin-nurse.servicein a NixOS VM. - Waits for port 4000 to open.
- Hits
GET http://localhost:4000/_healthand asserts success.
nix flake check --impureThis is also what CI runs (see .github/workflows/ci.yml). There is no way to run a
"single test" in the traditional sense — the full flake check is the test suite.
export DATABASE_URL=postgres://user:password@localhost/penguin_nurse
./lint.sh
# or:
DATABASE_URL=postgres://... nix run .#lintAll code targets Rust 2024 edition (edition = "2024" in Cargo.toml).
- Use
cargo fmtbefore committing. No customrustfmt.toml. - Add
#[rustfmt::skip]sparingly for macros/enums where formatting would be harmful (e.g., theRouteenum insrc/main.rs).
UI-heavy files (components, forms) include #![allow(non_snake_case)] at the top
because Dioxus components must be PascalCase functions.
Group imports as follows, separated by blank lines:
std::*- External crates (alphabetical)
crate::*(internal)
Example:
use std::{num::ParseIntError, str::FromStr};
use chrono::{DateTime, Utc};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{
forms::fields::InputString,
models::consumables::Consumable,
};This applies to Dioxus components, operation enums, dialog variants, etc.
| Example | Meaning |
|---|---|
ConsumableUpdateBasic |
Update basic fields of a Consumable |
ConsumableUpdateIngredients |
Update ingredients of a Consumable |
ConsumableDelete |
Permanently delete a Consumable |
ConsumableCreate |
Create a new Consumable |
- Add — add an item to a list
- Remove — remove an item from a list
- Create — create a new entity (persisted)
- Archive — non-permanently hide/archive an entity
- Delete — permanently delete an entity
- Update — update an entity or value
- View — view an entity or value
- Change — pending (unsaved) changes to an entity
- New — pending (unsaved) entity to be created
| Kind | Convention | Example |
|---|---|---|
| Structs / Enums | PascalCase |
Consumable, MaybeSet, ConsumableUnit |
| Dioxus components | PascalCase |
ConsumableUpdate, InputString |
| Functions / methods | snake_case |
create_consumable, get_user_id |
| Variables | snake_case |
consumable_id, active_dialog |
| Server functions | snake_case (with #[server]) |
delete_nested_consumable |
| Constants | SCREAMING_SNAKE_CASE |
ORGANIC_SVG, TAILWIND_CSS |
| Files | snake_case.rs |
consumables.rs, health_metrics.rs |
| Validation functions | validate_<field> |
validate_name, validate_blood_glucose |
Each domain entity has three model structs:
Foo— the read/display model returned from the databaseNewFoo— the creation model (all required fields)ChangeFoo— the update/patch model usingMaybeSet<T>fields
A custom enum in src/models/common.rs used for partial updates (patch semantics).
Use it instead of Option<T> when a field can be explicitly set to None vs. left
unchanged.
All entity IDs are newtype structs over i64:
pub struct ConsumableId(pub i64);This prevents accidental ID cross-assignment at compile time.
All model structs derive: Serialize, Deserialize, Debug, Clone, PartialEq, Eq.
Add Hash when the type will be used in a HashMap/HashSet.
FieldValue— bidirectionalString↔ typed conversion for form fields (src/forms/values.rs)FieldLabel— renders an enum variant as a human-readable label for dropdownsAllValues(fromderive_enum_all_values) — enumerates all variants of an enum
-
Use
thiserrorfor all error types. Derive#[derive(Error, Debug)]. -
AppError(src/functions/common.rs) is the top-level server error enum. It#[from]-converts database and bb8 pool errors. -
Server function error chain: always end with:
.map_err(AppError::from) .map_err(ServerFnError::from)
-
ValidationError(String)— simple newtype for form field validation errors. -
EditError— wraps either aServerFnErroror aValidationError, used as the error type in form save operations. -
Savingenum — models async form submission state:No|Yes|Finished(Result<(), EditError>). -
Use
?freely. Use.optional()for database queries that may return zero rows. -
No panics in normal flow. Only
expect()for mandatory server extensions (startup-time assertions).
Every #[server] function follows this exact pattern:
#[server]
pub async fn do_thing(arg: Arg) -> Result<Output, ServerFnError> {
let user_id = get_user_id().await?; // 1. authenticate
let mut conn = get_database_connection().await?; // 2. get DB connection
// 3. delegate to src/server/database/models/
db::do_thing(&mut conn, user_id, arg)
.await
.map(|x| x.into()) // 4. convert DB model → frontend model
.map_err(AppError::from) // 5. map errors
.map_err(ServerFnError::from)
}- State:
use_signal,use_memo,use_resource,use_callback - Async side-effects:
spawn(async move { ... }) - Dialog state: an
ActiveDialogsignal holding an enum of which dialog is open - Template:
rsx!{ ... }macro with Tailwind + DaisyUI class strings - Components with validation use a
Validatestruct fromsrc/forms/holding aMemo<Result<T, ValidationError>>per field
#[cfg(feature = "server")]— gates all server-only code (Diesel, Axum, auth). Model files use this to conditionally add Diesel derives.cli-only— feature used to build thelintbinary without the full web stack.
The tap crate's .pipe(fn) is used for readable method-chaining of results and
values. Prefer .pipe(Ok) over wrapping in Ok(...) at end of expression chains.