Skip to content

An Original React Booking Capstone

Step 36 of 45

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:

  1. Write a one-paragraph product brief with a target user and a measurable outcome.
  2. Split features into must-have, should-have, and later using a simple prioritization rule.
  3. Convert features into user stories with testable acceptance criteria.
  4. Model the data of the application (entities, fields, relationships) before building screens.
  5. Plan routes, a component tree, and the state ownership of each piece of data.
  6. Choose the project structure, tooling, and a Git workflow for a solo project.
  7. 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. Store 4500 and format as a currency string only at display time with Intl.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:

  1. Skeleton: Vite project, routing with placeholder pages, header and footer, design tokens. Deployable immediately.
  2. Catalog slice: data module, WorkshopCard, list, semantic markup, responsive grid.
  3. Detail slice: route parameter, not-found handling.
  4. Filter slice: level filter stored in the URL.
  5. Booking slice: form, validation, state, confirmation.
  6. 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