Skip to content
forma.
v3.0
☰

Lesson 08 / A complete application

Make the inventory
durable.

The browser lessons taught local state. Now make the server authoritative, preserve ordinary forms and carry an edit through to storage.

Download Field Notes ↓Inspect the source →

The indexed-list lesson changes stock in the current browser page. Those changes disappear on refresh because nothing saves them. Field Notes adds a write path: Rust validates an edit and writes a JSON file before confirming it to the browser. By the end of this lesson, you will distinguish a draft from a saved value and prove that an accepted edit survives both a page reload and a server restart.

Rust renders the initial inventory. Three item editors become islands; the rest of the page remains ordinary server HTML.

Run the complete application#

Before running these commands, install Node 22+ with npm and Rust 1.85+ with Cargo. The download contains application source; it does not install either toolchain. Follow the installation guide if node, npm, rustc or cargo is not available in your terminal.

Download the project above and extract it. Open a terminal in the directory containing the extracted field-notes folder:

sh
cd field-notes/admin
npm ci
npm run build
cd ..
CARGO_BUILD_JOBS=3 cargo run --locked

npm ci installs the frontend versions recorded in its lockfile. npm run build produces the browser bundles and native FMIR together. cargo run --locked then builds and starts Rust using the Cargo lockfile and those frontend assets. CARGO_BUILD_JOBS=3 limits compilation to three jobs; it is not a server setting.

Open http://127.0.0.1:4176. A first run shows three items, including eight field notebooks. Later runs load your saved inventory. The site's tested toolchains are Node 24.22.0 and Rust 1.93.1. This project's build command always selects native SSR; unlike the official minimal scaffold, it does not need a separate build:ssr command.

The default inventory file is data/inventory.json, relative to the directory where you start the server. Rust loads it at startup and maintains an in-memory snapshot while running. Accepted edits update the file and then that snapshot. Set DATA_FILE to choose another file and PORT to choose another local port. Keep one process per inventory file.

Know which value you are looking at#

What you seeWhere it livesWhen it changes
Quantity and note fieldsThis page's editable draftAs you type; typing does not save
“Confirmed in stock”The last quantity this editor received from RustOn page load or a successful save
Saved inventoryRust's snapshot and the JSON fileOnly after validation and a successful write

The confirmed count is not a live feed from other tabs. Reload inventory requests current data and discards unsaved fields; copy a draft first if you need it. That explicit link also works after a failed ordinary form submission.

Follow one item across the boundary#

Read admin/src/app.tsx beside src/main.rs. TypeScript declares ItemView; Rust derives its matching PageProps. Rust's draft_quantity field becomes draftQuantity in the declared page contract.

For each page request, the handler reads the current inventory snapshot and passes initial values and form URLs to a bound page handle. A keyed list identifies each item by its business ID. activateIslands({ StockEditor }) attaches behavior to the existing editors; it does not replace the server's inventory markup.

Each editor seeds signals from its server props. Saving temporarily disables the fields, so a response cannot overwrite newer typing. The confirmed count stays unchanged until Rust accepts the write, then receives the accepted quantity and new version. A refused or unconfirmed request leaves the draft visible.

Exercise 1: prove the page exists before JavaScript#

Disable JavaScript and reload. Stock and shelf notes should already be present: Rust supplied them in the document. Change the notebook quantity to 12 and choose Save changes. The POST saves the edit; a redirect then requests fresh HTML. Its fragment keeps the notebook editor in view instead of returning to the hero.

Check: both the confirmed count and the input show 12. Use Reload inventory and inspect page source: 12 is still present. Re-enable JavaScript.

Exercise 2: make a confirmed edit#

Type 15 and a shelf note. Before saving, the confirmed count should still show 12: you have changed the draft, not the stored item. Choose Save changes. The same editor reports “Saved on the server.” and its confirmed count becomes 15. With JavaScript enabled, this POST uses a JSON response instead of navigating.

Stop Rust with Ctrl+C and run CARGO_BUILD_JOBS=3 cargo run --locked again from the same field-notes directory. Open a fresh page after the restart.

Check: 15 and the note return from the file. An unchanged browser tab alone would not prove this; it could still be showing its old in-memory value.

Exercise 3: discover a conflict#

Open two tabs before changing anything. In the first tab, save a new quantity. In the second, submit its older version. The server refuses the stale update with 409 Conflict and leaves the newer stored item intact. The second tab's draft remains visible. Copy anything you want to keep, choose Reload inventory, and decide what to edit using the latest item. The link fetches a new document and returns to that item. It does not retry the stale POST.

Check: a third freshly opened tab shows the first tab's accepted value. With JavaScript disabled, submitting the rejected draft again still returns 409: rendering an error page does not give an old draft the latest write version. The version detects stale edits; it does not identify or authorize a user.

Exercise 4: change a rule at every boundary#

In your disposable learning copy, lower the quantity limit from 9,999 to 99. Update the request parser and stored-file validation in src/store.rs, then the input's max in admin/src/app.tsx. Browser bounds guide editing; the Rust checks enforce the rule even when callers bypass the form. Add tests accepting 99 and refusing 100, alongside the existing empty, negative and fractional cases.

If your saved inventory already contains a quantity over 99, intentionally adjust it while the original application still accepts it, or use a new disposable DATA_FILE. Do not silently reset existing data to make the new validation pass. Run the checks from field-notes:

sh
CARGO_BUILD_JOBS=3 cargo test --locked

Then rebuild admin and restart Rust so its embedded input markup matches the new validator. Check: 99 saves and survives restart; 100 is refused by Rust when browser validation is bypassed, and the stored value stays 99. Trace that rejection through src/main.rs: it must return before Store::update can write.

Read the write path#

The normal form and the enhanced fetch submit the same fields: quantity, note, version and a per-process request token. The server checks the request origin and token, validates the edit, checks the item's version and serializes writes. It writes a replacement file before changing its in-memory snapshot and confirming success. Accept: application/json asks for the enhanced response; an ordinary form gets a redirect on success or an HTML error page that retains the draft.

A network failure is different from a refused write. The response may have been lost after Rust saved the file. The editor therefore says it could not confirm the save; use Reload inventory to check before trying again.

The token is a cross-site request protection for this local demo. It does not identify users or authorize access. Field Notes has neither user accounts nor tenant boundaries, so it deliberately listens only on loopback.

What the tests prove#

The Rust checks exercise invalid input, disk reload, stale edits, concurrent writes, write failure and corrupt-file refusal. Browser checks use isolated data files and actual processes to verify initial HTML, retained editor nodes, server-confirmed saves, no-JavaScript forms, conflicts and persistence after restart.

Those are application tests. The framework's own release qualification is separate. The complete source is available on the example page, and the testing guide explains how to carry these checks into your own application.

Choose what to change next#

This is a small, single-process store for learning. It does not provide backups, cross-process locking, a multi-instance database or user authorization. Before publishing an administration interface, add authentication and access rules, choose durable storage appropriate to the deployment, and test recovery.

For the next lesson, retain the demonstrated contract: validate on the server, render useful HTML, adopt only the interactive regions and confirm mutations only after the server accepts them.

Find your next answer.

Search Forma 3.0 documentation, guides and examples.