[Guide] Build a Job Board - implementation notes
For multi-step flows: do you prefer putting the stepper in route state (session slug + step key) or keeping it local and only writing on submit?
This guide models an application as a route session identified by a random slug. The URL identifies the session; route state drives the stepper; the application document stores derived review evidence (missing fields, completion percent).
How are you generating and persisting session slugs (pure client createSlug(), or server-issued)? Do you treat step transitions as route truth (route.write('step', ...)) or as local UI motion? When do you commit form state (blur vs keystroke), and how do you keep review screens deterministic? What derived evidence do you store for review screens (summary strings, missing fields, completion percent)?
Comments (10)
Back to latestWhy random slugs for the application route instead of /jobs/123/apply?
Is the point to avoid coupling the apply flow to the job id?
Exactly. The session slug is a stable handle for the application flow, which can outlive the job page shape.
It also makes 'resume application' a clean route story without encoding meaning in the URL.
Makes sense. I was thinking it was just a "pretty URL" thing.
On a larger team, the real win is that the stepper becomes route truth.
If steps are state, they're testable and snapshot-able, and any segment can self-correct the flow.
Storing derived evidence for review prevents a lot of 'expensive recompute on submit'.
It also avoids weird cases where review renders different missing fields than the step validations.
I like that the apply doc has a stable shape (applicant, experience, answers, consents).
When the doc is stable, you can change the UI flow (file routes vs component routes) without migrating data models.
Submit-on-blur can surprise users if they're used to a big final submit.
We ended up using blur to commit into state (for stable review), but still gated the network mutation behind an explicit 'Submit' action.
In prod, the 'missing fields' list as stored derived state is huge for support/debugging.
We surfaced it as a debug panel for recruiters too. It made triage fast.
Yes. Derived evidence is not just perf—it’s observability.
If the route can render it, the route can explain itself.
Question: do you ever allow step skipping?
We used allowSkip as a route key and then derived missingFields so review always stayed honest even if users jumped around.
Testing: route-flow tests feel especially natural for steppers.
Snapshots at after-blur, after-review, after-submit give you a durable audit trail of how the apply contract behaves.
For session slugs: we kept it client-generated but treated it as a contract key (and rendered it as evidence). The main goal wasn't security, it was reproducibility and support: a screenshot should include the session handle.
ts
export function createSlug() {
const a = Math.random().toString(16).slice(2, 10);
const b = Date.now().toString(16).slice(-6);
return `${a}-${b}`;
}
route.write('sessionSlug', route.read('sessionSlug') ?? createSlug());
// Render it in the shell so screenshots/tests capture it:
// <section data-session={route.read('sessionSlug')} />One thing I liked in the guide is separating step motion from step truth. We model truth as a small union and treat motion as a derived consequence, which keeps back/forward deterministic.
ts
type Step = 'profile' | 'experience' | 'questions' | 'review';
type ApplyDoc = { step: Step; completed: Step[]; missingFields: string[] };
function canEnter(step: Step, doc: ApplyDoc) {
return step === 'review' ? doc.missingFields.length === 0 : true;
}
It feels overly formal until you debug a bug report where the user jumped around and ended up in review with half the doc missing.