Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Guided Build: Sprout, End to End  ›  Capstone Project

Grow in Vertical Slices

Checkpoint — Deployment 16 minAfter the skeleton, each feature is added as a vertical slice that changes the data, a guarded route, the screen and the tests together, and is merged and deployed on its own.

Grow in Vertical Slices

With the skeleton live, Sprout grows one feature at a time. Each feature is a slice: a thin cut through every layer (table, route, screen and test) that is merged and deployed on its own. Here is one whole slice from the plan, start to finish: watering a plant and keeping its history. Every step uses something from an earlier section.

The rule

Each feature cuts through every layer and ships on its own.

Data. A new migration adds the history table. The database enforces the basics:

CREATE TABLE waterings (
  id         serial PRIMARY KEY,
  plant_id   integer NOT NULL REFERENCES plants(id) ON DELETE CASCADE,
  watered_on date NOT NULL DEFAULT CURRENT_DATE
);

API. A guarded route that creates a watering, but only on the user's own plant:

app.post('/api/plants/:id/waterings', requireUser, async (req, res) => {
  const { rows } = await pool.query(
    'UPDATE plants SET last_watered = CURRENT_DATE WHERE id = $1 AND owner_id = $2 RETURNING *',
    [req.params.id, req.user.id]
  );
  if (rows.length === 0) {
    return res.status(404).json({ error: { code: 'not_found', message: 'Plant not found' } });
  }
  await pool.query('INSERT INTO waterings (plant_id) VALUES ($1)', [rows[0].id]);
  res.status(201).json(rows[0]);
});

The owner check in the UPDATE does double duty: it guards the row and finds the plant. In production code, run the two statements inside one transaction (BEGIN ... COMMIT) so they succeed or fail together.

Screen. The "Water now" button follows listen, send, check, update, and swaps in the server's version of the plant:

async function water(plant) {
  const res = await api(`/api/plants/${plant.id}/waterings`, { method: 'POST' });
  if (!res.ok) return setError('Could not record the watering.');
  const updated = await res.json();
  setPlants((list) => list.map((p) => (p.id === updated.id ? updated : p)));
}

Tests. Three API tests describe the rules: the owner gets 201 and today's date; another user gets 404; a logged-out request gets 401.

Ship. A pull request runs CI. After the merge, the pipeline migrates first, so the table exists before the new code starts, then restarts and checks health. Finally, water a real plant on the live site.

Slices are small on purpose. Each one can be reviewed in a single sitting, and when something breaks after a deploy, you know exactly which change to look at. Twenty features shipped as twenty slices are far easier to trust than one big release that touched everything at once.

The shortcut

A slice is done only when it passes the whole checklist: migration, guarded route, screen with every state, tests for success and for someone else's data, and verified live. Missing any step means you built a layer, not a feature.

The watering slice, checked offStep 1 of 4
  1. Data

    The migration runs locally, and waterings exists with its foreign key to plants.

Drills

Answer fast — trust the rule.

quick drill

What makes this a vertical slice rather than a layer?

quick drill

The route answers 404 when Asha tries to water Ben's plant. Which part of the handler causes that?

quick drill

Why does the client use the plant from the response instead of setting today's date itself?

quick drill

During deployment, why must the migration run before the API restarts?

Remember

  • Each feature cuts through every layer and ships on its own.
  • Shortcut: done means migration, guarded route, full-state screen, tests and a live check.
  • Trap: calling a feature done when only one layer is finished.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainPlan the reminder slice

Plan Sprout's next slice: 'email me when a plant is thirsty'. Explain what changes in each layer (data, server, email sending, screen, tests) and how the slice is shipped.