Build a Production Frontend
The capstone. A brief, the requirements that make it harder than it sounds, a fixed order of questions whose answers constrain each other, and then the things that go wrong while you are still answering them.
Build the frontend for a multi-tenant SaaS application used by organisations across several continents, on hardware ranging from current laptops to four-year-old Android phones, on networks ranging from office fibre to hotel wifi. It has a dashboard, search, large tables, file uploads, realtime notifications, drafts that must survive losing connectivity, and an AI assistant. Every user is inside an organisation, and every organisation believes its data is the only data. Build it — but decide each thing only once, be able to say which requirement forced it, and remember that the tab you ship to may still be open a week after the API changes.
The requirements
Each one sounds routine and has a wrinkle in it that decides most of the design.
The client can present identity and cannot enforce it. Where the credential lives is a trade between two attacks, not a preference: a script-readable token is reachable by any XSS on the page, and a cookie is attached automatically to requests your code did not initiate. Whichever you choose, the frontend still has to survive expiry mid-action without discarding the work in progress.
A dozen widgets, each with its own data, its own refresh cadence and its own failure mode. Fetched independently they produce a dozen spinners resolving at different moments; fetched together the slowest one holds the whole screen. The real question is which of them are the same server state seen twice.
Every keystroke is a request that may be answered after the one that followed it. Responses arrive in network order, not request order, so the last answer to land can belong to a query the user has already changed — and the result looks like a rendering bug rather than an ordering one.
Tens of thousands of rows, sortable and filterable. Rendering them all makes style, layout and memory scale with rows nobody can see; virtualising them breaks find-in-page, anchor links and the table semantics assistive technology relies on, so the accessibility work has to be redone deliberately rather than inherited.
Uploads are the one interaction that reliably outlives the view that started it. It needs real progress, cancellation, retry of a partial transfer, and a decision about what happens when the user navigates away — and the same file dropped twice must not become two records.
A socket is a second source of truth arriving on its own schedule. It delivers at least once, out of order across reconnects, and goes silent in a way that looks identical to nothing having happened. The UI has to reconcile it against a cache that was populated by a different transport.
Editing must continue when the network does not, which means queued mutations, an ordering guarantee across them, and an answer for the draft that was written against a record someone else has since changed. Offline is not a network state — it is a conflict-resolution requirement.
Every custom control — the combobox, the dialog, the tab set, the sortable header — has a specification for keys, focus and announcement that a component library may implement partially. Automated audits find missing labels and bad contrast; they cannot find that focus was dropped when a panel closed.
Translated strings are longer, shorter, and sometimes right-to-left, which makes fixed-width layouts and concatenated sentences break in ways the source language never shows. Dates, numbers and currencies are the tenant's locale and the user's locale, and those are not always the same one.
A timestamp rendered in the browser's zone and a timestamp rendered on the server in UTC disagree, which is a hydration mismatch and a support ticket at once. "Today" is a different range for two users looking at the same dashboard, and a date-only value has no timezone at all — converting it is the bug.
Tokens stream in, tools report progress, results arrive partial, and the user may cancel mid-answer. Every one of those is a UI state that a request-response component model does not have — and a model proposing an action is never the same thing as that action being authorized.
Each organisation supplies colours and a logo. Applied as inline overrides they defeat the cascade and multiply specificity; applied as custom properties on the root they invalidate the computed style of everything that inherits them on every change. And a tenant colour that fails contrast is an accessibility regression you shipped on their behalf.
The questions
Answer each one out loud before opening it. The answer is much less useful if you have not first committed to one of your own.
- 1
Which rendering strategy does this application use, and per surface or globally?
- 2
Where does state live?
- 3
What belongs in the URL?
- 4
What is server state, and what makes it different?
- 5
What is cached, and where?
- 6
How does invalidation work?
- 7
How are errors shown?
- 8
How is accessibility guaranteed rather than intended?
- 9
How does the browser actually render this?
- 10
What ships as JavaScript?
- 11
What happens between content appearing and the page responding?
- 12
How are the large tables handled?
- 13
How are realtime events reconciled with the cache?
- 14
How do tabs that have been open for a week survive backend deploys?
Injected failures
Each of these arrives while the product is live. Every one has a response that looks right and either makes it worse or leaves the mechanism intact — read the trap even when you got the cause.
First visits are much slower, especially on low-end Android devices. Repeat visits on the team's laptops are unchanged, so nobody notices for a month.
Content arrives noticeably later in the field. Total transferred bytes went down. The release contained a new font, a marketing tag and a stylesheet reorganisation.
Content moves shortly after it appears, most often on the dashboard, and occasionally a click lands on the wrong thing — a support ticket about a wrong action taken, not about a visual glitch.
Rare, unreproducible locally, and much more common on hotel wifi. The result list contradicts the text in the input, and refreshing fixes it.
Duplicate rows in the notification list, and a badge count that drifts upward over a long session until a reload corrects it.
A console warning during development, and in production a flash where the server-rendered timestamps are replaced with different text, plus occasional lost event handlers on the affected subtree.
The report is "it does nothing". Every automated accessibility check passes, and the flow works perfectly with a mouse.
A spike of client-side errors from a minority of sessions, all of them long-lived tabs. New sessions are unaffected, so the deploy looks healthy on every server dashboard.
The application is fine for an hour and sluggish by the afternoon. Interactions get gradually worse, the tab eventually reloads itself, and nothing appears in the error tracker.