Scope and Design the Capstone
Introduction
This is lesson 1 of chapter 8, the start of the capstone part of the Meta Front-End Developer pathway (course 8 in the official program, domain capstone). The earlier chapters taught the parts: JavaScript, Git, semantic HTML and CSS, React components, state, hooks, testing, and UX. The capstone asks you to combine them into one coherent, working product. Employers and reviewers rarely ask what you know; they ask what you have shipped.
Capstones fail less often from missing technical skill than from missing scope. Learners start coding a ten-page application, run out of energy at the third page, and end with a pile of half-finished screens. This lesson is about the work that happens before the first component: choosing a scope you can finish, turning it into requirements, designing the data and the screens, and planning the order of work.
The project is the original Harbor Workshops booking interface, a fictional community center that offers workshops such as pottery, bike repair, and bread baking. All briefs, data, and code in this chapter are OnLevelUp materials. They do not reproduce any graded project from the official program, and finishing them does not grant the Meta credential, which is awarded through the official program on Coursera.
Learning objectives
By the end of this lesson you can:
- Write a one-paragraph product brief with a target user and a measurable outcome.
- Split features into must-have, should-have, and later using a simple prioritization rule.
- Convert features into user stories with testable acceptance criteria.
- Model the data of the application (entities, fields, relationships) before building screens.
- Plan routes, a component tree, and the state ownership of each piece of data.
- Choose the project structure, tooling, and a Git workflow for a solo project.
- Break the work into small vertical slices and order them to reduce risk.
1. Start with a brief, not with code
A product brief is a short document that keeps every later decision honest. It has four parts: who the product is for, what problem it solves, what "done" means, and what is deliberately out of scope.
Harbor Workshops brief:
Who: Residents of a small town who want to find a hands-on workshop and reserve seats, mostly from a phone. Problem: The center currently takes bookings by phone and a paper sign-up sheet. People do not know which sessions have seats, and staff double-book rooms. Done when: A visitor can browse workshops, filter by level, open a detail page, choose a number of seats, submit a booking form with validation, and see a confirmation. The interface is responsive, keyboard accessible, and tested. Out of scope: Payments, user accounts, an admin back office, and real-time seat sync across devices.
The out-of-scope line is the most valuable. Every item on it is a decision to say no now, so that you can say yes to finishing.
Choosing a scope you can finish
Estimate in vertical slices, meaning a thin piece of the product that works from the screen to the data, not a full layer. "The catalog page with real data and a filter" is a slice. "All the CSS" is not.
A practical rule is the MoSCoW split:
| Category | Meaning | Harbor Workshops example |
|---|---|---|
| Must have | Without it the product does not work | Catalog, detail page, booking form, confirmation |
| Should have | Important, but the product still works without it | Level filter stored in the URL, seat availability warnings |
| Could have | Nice if time allows | Dark theme, saved favorites in localStorage |
| Won't have (now) | Explicitly postponed | Payments, accounts, admin |
Plan so that the must have items finish with time to spare. A finished small product always beats an unfinished large one.
2. From features to user stories
Chapter 7 introduced the user story: "As a who, I want what, so that why," paired with acceptance criteria that can be verified. Stories keep you from building features nobody needs; criteria become your test cases in lesson 4.
| Story | Acceptance criteria |
|---|---|
| As a visitor, I want to see all upcoming workshops so I can choose one | The catalog lists every workshop with title, date, level, price, and seats left; an empty result shows a helpful message |
| As a visitor, I want to filter by level so I only see suitable workshops | Choosing "Beginner" shows only beginner workshops; the choice is in the URL, so reloading keeps it; "All" clears it |
| As a visitor, I want to see details so I can decide | The detail page shows description, instructor, date and time, price, and seats left; an unknown id shows a not-found page |
| As a visitor, I want to book seats so I can attend | The form requires name, a valid email, and 1 to the available seats; invalid fields show a clear message tied to the field; success shows a confirmation with a summary |
| As a keyboard user, I want to complete a booking without a mouse | Every control is reachable with Tab in a logical order; focus is always visible; errors are announced |
Two cautions. First, a criterion such as "the page is user friendly" cannot be verified; rewrite it as something observable. Second, criteria should describe behavior, not implementation. "Uses useReducer" is a technical decision, not a user need.
3. Model the data first
Before drawing screens, decide what the data looks like. A user interface is mostly a view of data plus a way to change it. If the data model is wrong, every component suffers.
Harbor Workshops needs two entities in the first version:
// A workshop (read-only in this version)
{
id: 'w-101', // stable identifier used in URLs
title: 'Beginner Pottery',
level: 'beginner', // 'beginner' | 'intermediate' | 'advanced'
startsAt: '2026-11-14T10:00:00',
durationMinutes: 150,
priceCents: 4500, // money in integer cents, never floats
capacity: 12,
seatsTaken: 9,
instructor: 'Mara Ellis',
summary: 'Shape and glaze your first bowl.'
}
// A booking (created by the user)
{
id: 'b-2001',
workshopId: 'w-101',
name: 'Sam Rivera',
email: 'sam@example.com',
seats: 2,
createdAt: '2026-10-18T09:12:00'
}
Notice several good habits:
- Money in integer cents. Floating-point arithmetic gives results like
0.1 + 0.2 === 0.30000000000000004. Store4500and format as a currency string only at display time withIntl.NumberFormat. - Derived values are not stored. Seats left is
capacity - seatsTaken. If you store it as well, the two numbers can disagree. - Stable ids. The id appears in the URL (
/workshops/w-101), so it must not be an array index that changes with sorting. - Dates as ISO strings. They sort correctly and parse reliably.
For the first version, the data can live in a local module (workshops.js) or a JSON file served by a tiny mock function. The shape matters more than the source. A later step can replace the source with a fetch call without touching the components.
The data layer as a function
Wrap data access in functions so components never know where the data comes from:
// src/api/workshops.js
import { workshops } from '../data/workshops';
export function listWorkshops({ level } = {}) {
const all = workshops;
return level ? all.filter((w) => w.level === level) : all;
}
export function getWorkshop(id) {
return workshops.find((w) => w.id === id) ?? null;
}
export function seatsLeft(workshop) {
return workshop.capacity - workshop.seatsTaken;
}
Today these return values directly. Tomorrow they can return promises from fetch. The only change in the UI is adding loading and error states, which lesson 3 covers.
4. Design the screens and the routes
Use the wireframing skills from chapter 7 on paper or in a tool, but keep the result small. For each screen, write down its purpose, its data, and its main action.
| Route | Screen | Data | Main action |
|---|---|---|---|
/ |
Home | Short intro, featured workshops | Go to the catalog |
/workshops |
Catalog | Workshop list, level query parameter |
Open a workshop |
/workshops/:id |
Detail | One workshop | Start booking |
/workshops/:id/book |
Booking form | One workshop, form values | Submit booking |
/confirmation |
Confirmation | The submitted booking | Return to the catalog |
* |
Not found | None | Go home |
Think about states on every screen, not only the happy path. For each, list loading, empty, error, and success where they apply. Many weak portfolios show only the success state.
Responsive thinking
Design mobile first: the single-column layout is the base, and wider layouts add columns with min-width media queries or container-friendly CSS grid (repeat(auto-fill, minmax(16rem, 1fr))). Mobile first forces you to prioritize, because there is no room for clutter.
Visual system in small decisions
Choose, once, a small set of design tokens and write them as CSS custom properties:
:root {
--color-bg: #fbfaf7;
--color-text: #1f2933; /* contrast above 12:1 on --color-bg */
--color-primary: #0b5cad; /* above 4.5:1 with white text */
--color-danger: #a4262c;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--radius: 0.5rem;
--font-body: system-ui, sans-serif;
}
Tokens keep the design consistent and make changes cheap. Check the contrast of text and button colors against the WCAG ratios from lesson 7.3 before writing components.
5. Plan the component tree and state ownership
From the screens, extract components. A useful top-down method: draw boxes around parts of the wireframe, name each box, and ask whether it repeats or has its own behavior.
<App>
<Header /> // site name + <nav>
<main>
<Routes>
Home
Catalog
<LevelFilter />
<WorkshopCard /> (repeated)
WorkshopDetail
BookingPage
<BookingForm />
Confirmation
NotFound
</main>
<Footer />
</App>
Then decide, for each piece of data, who owns it. The rule from chapter 5 still applies: keep state as low as possible, lift it only as far as needed.
| Data | Owner | Why |
|---|---|---|
| Selected level filter | URL query parameter | Shareable, survives reload, back button works |
| Form field values | BookingForm (local state or reducer) |
Only that component needs them while typing |
| Submitted booking | A parent (App or a context) |
Both the booking page and the confirmation page need it |
| List of bookings | App-level reducer/context | Reduces duplicate seats and drives seat counts |
| Workshop data | Data module / fetch layer | External source of truth |
If you are unsure, start local and lift when a second component needs the value.
6. Tooling, structure, and Git
Create the project with Vite and the React template. Vite is the standard fast build tool for new React learning projects; do not start new work with Create React App, which is no longer recommended for new projects. Check the current Vite and React documentation for supported versions when you start.
A clear structure:
harbor-workshops/
src/
api/ // data access functions
components/ // shared UI: Header, Footer, WorkshopCard
pages/ // route-level screens
hooks/ // custom hooks such as useBookings
styles/ // tokens and global CSS
test/ // test helpers
App.jsx
main.jsx
index.html
README.md
Git workflow for a solo project: commit small and often with meaningful messages ("Add level filter to catalog"), keep main working, and use a short-lived branch per feature (feature/booking-form) merged when its criteria pass. Write a README early: what the project is, how to run it, how to test it, and the decisions you made. A reviewer reads the README first.
Add linting (eslint) and formatting from day one. Consistency is cheaper to keep than to restore.
7. Plan the work in vertical slices
Order slices so that the riskiest or most valuable work happens first:
- Skeleton: Vite project, routing with placeholder pages, header and footer, design tokens. Deployable immediately.
- Catalog slice: data module,
WorkshopCard, list, semantic markup, responsive grid. - Detail slice: route parameter, not-found handling.
- Filter slice: level filter stored in the URL.
- Booking slice: form, validation, state, confirmation.
- Quality slice: tests, accessibility pass, performance pass, README.
After each slice, the application should run and be demonstrable. If you run out of time, you have a smaller product, not a broken one.
Worked example: slicing the booking feature
Problem. The "booking" story feels big. Plan it as slices with a clear definition of done.
Step 1: list the pieces. Form with three fields; validation; seat limit based on the workshop; submit handler; confirmation page; seat count update.
Step 2: find the thinnest working path. A form with one hard-coded field that navigates to a confirmation page showing "Booked". This proves the routing and the submit flow.
Step 3: add fields and controlled state. Name and email with labels, then seats as a number input with min=1 and max={seatsLeft}.
Step 4: add validation. A pure function validateBooking(values, workshop) returns an object of messages. Pure functions are easy to unit test.
Step 5: add the data effect. On success, dispatch bookSeats to a reducer; the workshop's seats left shrinks; the confirmation page reads the booking.
Step 6: polish. Focus the first invalid field on submit, announce errors, disable double-submit.
Why this order and not "design all the CSS, then wire everything"? The thin path exposes integration problems (routing, state sharing) on day one, when they are cheap. Styling first would hide them behind pretty screens, and a misunderstood state structure discovered late forces a rewrite.
Common pitfalls and exam tips
- No scope line. Without an out-of-scope list, features multiply. Write it first.
- Starting with the home page hero. It is the least informative screen. Start with the screen containing the core data.
- Storing derived data. Seats left, totals, and filtered lists should be computed, not stored.
- Floats for money. Use integer cents.
- Index as id. Sorting or filtering breaks identity and URLs.
- Over-engineering state. Do not add Redux or a global store for data two components share; lifting state or one context is enough.
- Skipping the loading, empty, and error states. They are part of the design.
Exam tip: pathway-style assessments often show a requirement and ask which structure, route, or state placement satisfies it. Look for the option that keeps state at the lowest common owner, uses the URL for shareable state, and models data without redundancy.
Mini case study: the library room-booking app
A library wants an interface for booking study rooms: 6 rooms, 1-hour slots, no accounts, bookings by name and email. A learner proposes: login, payments, room photos gallery, calendar sync, a map of the building, and an admin panel, all in 3 weeks.
Tasks. Write the brief, choose the scope, and identify the first three slices.
Solution.
Brief. Students book a study room for an hour from their phone. Done when a visitor can see available slots for a day, pick a room and slot, submit name and email, and see a confirmation.
Scope. Must have: day view, slot list per room, booking form with validation, confirmation. Should have: date change in the URL. Could have: room photos. Won't have now: login, payments, calendar sync, map, admin panel.
Why. The ask has about six major systems; three weeks supports one coherent flow with quality. Accounts and payments need back-end work that a front-end capstone does not demonstrate, and each adds failure states.
First three slices. (1) Skeleton with routing and tokens; (2) day view listing rooms with slots from a local data module; (3) booking form leading to a confirmation. Data uses { roomId, startsAt, bookedBy }, with slot availability computed, not stored.
Key terms
- Product brief: short statement of audience, problem, success criteria, and exclusions.
- MoSCoW: must, should, could, won't prioritization.
- User story: a need stated from the user's point of view.
- Acceptance criteria: verifiable conditions that complete a story.
- Vertical slice: a thin, working piece spanning UI to data.
- Derived data: values computed from other state, not stored.
- Design token: a named design value, such as a color or spacing step.
- State ownership: which component holds a piece of state.
Summary checklist
- I can write a brief with audience, outcome, and an out-of-scope list.
- My features are prioritized, and the must-haves are small enough to finish.
- Each story has criteria I can test.
- My data model avoids redundant fields, uses stable ids, and stores money as integers.
- I have routes, a component tree, and an owner for every piece of state.
- I chose Vite, a folder structure, linting, and a simple Git workflow.
- My plan is ordered in vertical slices, with a deployable skeleton first.
Official sources
- Meta Front-End Developer Professional Certificate (Coursera): https://www.coursera.org/professional-certificates/meta-front-end-developer (opens in a new tab)
- Vite, getting started: https://vite.dev/guide/ (opens in a new tab)
- React, start a new project: https://react.dev/learn/creating-a-react-app (opens in a new tab)
- React, thinking in React: https://react.dev/learn/thinking-in-react (opens in a new tab)
- MDN, MDN Web Docs for HTML, CSS, JavaScript: https://developer.mozilla.org/ (opens in a new tab)
