Curriculum1 of 12
+ Module 18

Professional Practice & Deploy

Work the way an engineer on a team works: branches, reviews, and deploys.

Phase 3 · Full stack · 12 sections · about 16 minutes

+ 01 / 12

Module Promise

You will learn to work like a professional engineer: committing thoughtfully, writing PRs that explain why, reading code you didn't write, testing components, debugging methodically, deploying safely, and using AI as a thinking partner, not a replacement for thinking.

Code is read more than it's written. Debugging is a skill. Collaboration is everything.


+ 02 / 12

Part 1: The Git Mental Model

What Git Actually Is

Git is not about file history. Git is about narrative. Every commit is a sentence in the story of your codebase's evolution. When you come back to a line of code six months later and wonder "why is this here?", Git tells you:

  • When it appeared
  • Who added it
  • Why (in the commit message)
  • What else changed with it (the whole commit)

This is why commit messages matter more than the code itself.

Branches: The Parallel Universe

A branch is a sandbox. You can:

  • Try a feature without touching main
  • Abandon it without mess
  • Share it with teammates for feedback
  • Land it only when it's ready

The mental model:

+ JavaScript
1main (the production-ready universe)
2 ↳ your-feature-branch (your sandbox, isolated, safe)
3 └─ commit 1: add button component
4 └─ commit 2: add tests for button
5 └─ commit 3: integrate into header

When you're done, you merge the branch into main. Git replays your commits on top of main's latest work. This is a PR.

Commits: Atomic Units of Change

A good commit:

  • Does one thing
  • Is easy to review
  • Has a clear reason
  • Does not break the build

A bad commit:

  • Mixes feature + refactoring + bug fix
  • Changes 10 unrelated files
  • Has no message or a one-liner
  • Introduces a new bug

Example bad commit history:

+ JavaScript
1fix stuff
2WIP
3lol
4Revert "fix stuff"
5actually fixed it this time

Example good commit history:

+ JavaScript
1Add TextInput component with label and error state
2Add unit tests for TextInput keyboard handling
3Fix TextInput focus loss on parent re-render
4Integrate TextInput into LoginForm

Each commit stands alone. Each one is "done."

The Workflow: Branch → Commit → PR → Merge

+ MERMAID
1graph LR
2 A["git checkout -b your-feature"]
3 B["Write code, test locally"]
4 C["git add, git commit"]
5 D["git push -u origin your-feature"]
6 E["Create PR on GitHub"]
7 F["Code review, feedback"]
8 G["Address feedback, commit again"]
9 H["Merge PR into main"]
10 
11 A --> B --> C --> D --> E --> F --> G --> F
12 G -->|approved| H

+ 03 / 12

Part 2: Pull Request Descriptions

Why PRs Exist

A PR is not code review. A PR is asynchronous communication.

You're not saying "review this code." You're saying: "Here's what I changed, why I changed it, what it fixes, and how to test it. Please look for gaps in my thinking."

The Anatomy of a Great PR

+ MARKDOWN
1## Summary
2 
3Fixes #742: Add character limit validation to chapter titles.
4 
5Writers were creating 200-character chapter titles that broke EPUB
6formatting. This change enforces a 60-character limit with a visual
7counter.
8 
9## Why This Way?
10 
11- **Why not just truncate?** Truncation silently mangles meaning.
12 A warning + counter lets the author decide.
13- **Why 60 characters?** EPUB specs recommend ≤80; we chose 60 for
14 breathing room in mobile readers.
15- **Why not server-side only?** Client feedback is instant. No round-trip.
16 
17## Tradeoffs
18 
19- **Pro:** Real-time feedback, compliant with EPUB, users keep control.
20- **Con:** One more input validation rule to maintain.
21 
22## How to Test
23 
241. Open a book in the editor
252. Click any chapter title
263. Type 61+ characters
274. Confirm you see the red warning and a character count
285. Try to submit the form, it should be disabled
296. Delete one character (now at 60)
307. Confirm the warning disappears and form is enabled
31 
32## Checklist
33 
34- [x] Tests pass locally
35- [x] No console warnings
36- [x] Mobile viewport works (tested on iPhone SE)
37- [x] Lighthouse score 90+

The Three Questions Every PR Must Answer

  1. 1What did you change? (Be specific. "Bug fixes" is noise. "Fixed race condition in book sync" is useful.)
  2. 2Why did you change it? (Business value, user benefit, or technical debt. Connect to a user problem or issue number.)
  3. 3How do I verify it works? (Step-by-step test plan, not just "run npm test".)

Red Flags

  • No description. The code alone is noise without context.
  • "Fixes multiple bugs" without itemizing them. Scope creep. Split into smaller PRs.
  • No test plan. The reviewer has to figure out how to test it, and they might miss edge cases.
  • Unrelated changes. A PR that adds a feature and refactors three files is hard to review and hard to revert if something breaks.

+ 04 / 12

Part 3: Reading Unfamiliar Code

Why This Skill Matters

You will spend 80% of your time reading code, your own, teammates', and open-source libraries. Writing is the other 20%.

Debugging unfamiliar code, integrating third-party components, or jumping into a legacy feature all require this skill.

The Three-Level Reading Technique

Level 1: Map the Topography (2 minutes)

Open the file. Don't read line-by-line. Ask:

  • What type of code is this? (Component, utility, hook, API route?)
  • How long is it? (50 lines? 500?)
  • What's the overall shape? (One export? Many helpers?)
  • What are the key types/interfaces?

Example: Reading RichTextEditor.tsx (a component from makeEbook)

+ JavaScript
11. It's a React component. 'use client' at the top means it runs in the browser.
22. Roughly 1,000 lines (big).
33. Lots of helper functions and event handlers.
44. Props interface: `RichTextEditorProps` extends HTMLAttributes.
55. Key dependencies: React hooks, DOMPurify (sanitization), Radix UI.

This takes 60 seconds and saves you from drowning in line-by-line reading.

Level 2: Find the Seams (5–10 minutes)

Now zoom in on structure. Ask:

  • What are the main functions/sections?
  • What does the component return? (JSX shape)
  • Where does data flow in? (Props)
  • Where does data flow out? (Callbacks)

Example: RichTextEditor.tsx

Props come in:

  • value (the HTML string to edit)
  • onChange (callback when content changes)
  • onInlineEditRequest (for AI-assisted editing)
  • onComposeRequest (for AI-assisted compose)

JSX returns:

  • A toolbar (buttons for bold, italic, etc.)
  • A contentEditable div (the actual editor)
  • Event listeners for keyboard shortcuts

Data flow:

+ JavaScript
1User types → onChange fires → parent updates state → new value passes back down → editor re-renders

Level 3: Trace a User Action (10–15 minutes)

Pick one thing the component does. Trace it:

  • What happens when the user clicks the Bold button?
  • What happens when they press Cmd+K?
  • What happens when they paste text?

This is where bugs hide. Trace it step-by-step.

Example: User presses Bold

+ JavaScript
11. User clicks the "B" (bold) button
22. onClick handler fires: `execCommand('bold')`
33. Browser's contentEditable API toggles <strong> tags
44. onInput fires (browser event)
55. onChange callback fires with new HTML
66. Parent receives new HTML, updates state
77. Component re-renders with new value
88. Toolbar button sees new selection, updates active state

Each step is one line of code (or a few). But knowing the path is the whole skill.

The Debugger Is Your Eyes

Stop reading and start watching.

When you open Firefox/Chrome DevTools:

  • Go to the Elements tab and inspect the actual DOM
  • Go to the Console tab and inspect variables
  • Go to the Network tab and watch what the browser fetches
  • Set a breakpoint on a line and step through it

This turns abstract code into runnable code. You can watch it execute.


+ 05 / 12

Part 4: Component Testing

Why Component Tests Matter

A unit test for a utility function (capitalize("hello")"Hello") is easy. It's input → output.

But React components are noisy. They have:

  • Lifecycle (mounting, updating, unmounting)
  • User interaction (clicks, keystrokes, scrolls)
  • Async state (API calls, animations)
  • Dependencies (other components, hooks, global state)

Testing components means testing all of that. This is where bugs hide.

The Testing Pyramid

+ JavaScript
1 👨‍💻 E2E Tests (run the whole app, click buttons, verify UI)
2 /\
3 / \ 10% of tests, 50% of coverage
4 / \
5 /______\
6 🧪 Integration Tests (test one feature, multiple components)
7 / \
8 / \ 30% of tests, 40% of coverage
9 / \
10 /____________\
11 🔧 Unit Tests (test one function, one behavior)
12 / \
13 / \ 60% of tests, 10% of coverage
14 / \
15 /____________________\

For this module, we'll focus on unit + component tests, which are the foundation.

Example: Testing a Button Component

Start with the simplest possible component.

+ TypeScript
1// Button.tsx
2interface ButtonProps {
3 label: string;
4 onClick?: () => void;
5 disabled?: boolean;
6}
7 
8export function Button({ label, onClick, disabled }: ButtonProps) {
9 return (
10 <button
11 onClick={onClick}
12 disabled={disabled}
13 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
14 >
15 {label}
16 </button>
17 );
18}

Now test it:

+ TypeScript
1// Button.test.tsx
2import { render, screen, fireEvent } from '@testing-library/react';
3import { Button } from './Button';
4 
5describe('Button', () => {
6 it('renders with label', () => {
7 render(<Button label="Click me" />);
8 expect(screen.getByText('Click me')).toBeInTheDocument();
9 });
10 
11 it('calls onClick when clicked', () => {
12 const handleClick = vi.fn(); // Mock function
13 render(<Button label="Click me" onClick={handleClick} />);
14 fireEvent.click(screen.getByText('Click me'));
15 expect(handleClick).toHaveBeenCalledOnce();
16 });
17 
18 it('does not call onClick when disabled', () => {
19 const handleClick = vi.fn();
20 render(<Button label="Click me" onClick={handleClick} disabled />);
21 fireEvent.click(screen.getByText('Click me'));
22 expect(handleClick).not.toHaveBeenCalled();
23 });
24 
25 it('disables the button when disabled prop is true', () => {
26 render(<Button label="Click me" disabled />);
27 expect(screen.getByText('Click me')).toBeDisabled();
28 });
29});

What each test does:

  1. 1Renders with label, The component accepts a label and displays it. This is the happy path.
  2. 2Calls onClick when clicked, User interaction works. Uses vi.fn() to spy on the callback.
  3. 3Does not call onClick when disabled, Edge case. Disabled buttons shouldn't fire callbacks even if clicked.
  4. 4Disables the button, Another edge case. The disabled attribute is actually set on the DOM element.

Notice: We're testing behavior, not implementation. We don't care how the component renders it. We care that:

  • The label shows up
  • Clicks fire callbacks
  • Disabled state prevents interaction

Example: Testing a Form Input with Validation

Now something more complex.

+ TypeScript
1// TextInput.tsx
2interface TextInputProps {
3 label: string;
4 value: string;
5 onChange: (value: string) => void;
6 maxLength?: number;
7 error?: string;
8}
9 
10export function TextInput({
11 label,
12 value,
13 onChange,
14 maxLength,
15 error
16}: TextInputProps) {
17 const isAtLimit = maxLength && value.length >= maxLength;
18 
19 return (
20 <div className="flex flex-col gap-2">
21 <label className="text-sm font-medium">{label}</label>
22 <input
23 type="text"
24 value={value}
25 onChange={(e) => onChange(e.target.value)}
26 maxLength={maxLength}
27 className={`px-3 py-2 border rounded ${
28 error ? 'border-red-500' : 'border-gray-300'
29 } ${isAtLimit ? 'border-orange-500' : ''}`}
30 />
31 {isAtLimit && (
32 <p className="text-xs text-orange-600">
33 {value.length} / {maxLength} characters
34 </p>
35 )}
36 {error && <p className="text-xs text-red-600">{error}</p>}
37 </div>
38 );
39}

Tests:

+ TypeScript
1// TextInput.test.tsx
2import { render, screen, fireEvent } from '@testing-library/react';
3import { TextInput } from './TextInput';
4 
5describe('TextInput', () => {
6 it('renders label and input', () => {
7 render(
8 <TextInput
9 label="Email"
10 value=""
11 onChange={() => {}}
12 />
13 );
14 expect(screen.getByLabelText('Email')).toBeInTheDocument();
15 });
16 
17 it('updates value on user input', () => {
18 const handleChange = vi.fn();
19 render(
20 <TextInput
21 label="Email"
22 value=""
23 onChange={handleChange}
24 />
25 );
26 const input = screen.getByLabelText('Email');
27 fireEvent.change(input, { target: { value: 'test@example.com' } });
28 expect(handleChange).toHaveBeenCalledWith('test@example.com');
29 });
30 
31 it('shows character count at max length', () => {
32 render(
33 <TextInput
34 label="Title"
35 value="Hello"
36 onChange={() => {}}
37 maxLength={10}
38 />
39 );
40 expect(screen.getByText('5 / 10 characters')).toBeInTheDocument();
41 });
42 
43 it('displays error message', () => {
44 render(
45 <TextInput
46 label="Email"
47 value=""
48 onChange={() => {}}
49 error="Invalid email"
50 />
51 );
52 expect(screen.getByText('Invalid email')).toBeInTheDocument();
53 });
54 
55 it('applies error styling when error prop exists', () => {
56 render(
57 <TextInput
58 label="Email"
59 value=""
60 onChange={() => {}}
61 error="Invalid email"
62 />
63 );
64 const input = screen.getByLabelText('Email');
65 expect(input).toHaveClass('border-red-500');
66 });
67});

Key patterns:

  • Use screen.getByText() to find elements by human-readable text, not querySelector.
  • Use fireEvent to simulate user interaction.
  • Use vi.fn() to mock callbacks and assert they were called.
  • Test edge cases: empty input, max length, error state.

Running Tests

Add to package.json:

+ JSON
1{
2 "scripts": {
3 "test": "vitest",
4 "test:ui": "vitest --ui",
5 "test:coverage": "vitest --coverage"
6 }
7}

Run:

+ Terminal
1npm test # Watch mode, re-runs on save
2npm run test:ui # Visual test dashboard
3npm run test:coverage # Show code coverage %

+ 06 / 12

Part 5: Debugging with Method

Error Location Is a Hint

When something breaks, the error message tells you where to look. Most developers ignore it and thrash.

Example error:

+ JavaScript
1TypeError: Cannot read property 'chapters' of null
2 at getChapterCount (book.ts:45:12)
3 at renderEditor (page.tsx:120:5)

Translation:

  • Line 45 of book.ts: chapters is null when it shouldn't be.
  • Called from line 120 of page.tsx: That's where the bug manifests.

Start at line 45. Why is chapters null? Did something not initialize it? Is the shape wrong?

The Debugging Workflow

  1. 1Read the error. The stack trace is the roadmap.
  2. 2Reproduce it. Can you make it happen again? How?
  3. 3Inspect at the failure point. Set a breakpoint right before the error. Watch the variables.
  4. 4Trace backwards. How did the data get into this bad state?
  5. 5Fix the root cause. Not the symptom. The root.

Using Browser DevTools

Chrome / Firefox DevTools:

Open DevTools (F12). Go to Sources tab.

  1. 1Set a breakpoint. Click the line number where you want to pause.
  2. 2Trigger the bug. Do the action that breaks it.
  3. 3Step through. Use the step buttons: - Step Over (→): Execute one line - Step Into (↓): Enter a function call - Step Out (↑): Exit the current function
  4. 4Inspect variables. Hover over them or type in the Console
  5. 5Conditional breakpoints. Right-click line number: "Add conditional breakpoint" → chapters.length === 0

Example: You're debugging the RichTextEditor. The user types, but the text doesn't appear.

+ JavaScript
11. Open DevToolsSources
22. Set breakpoint on the onChange handler (line ~120)
33. Type in the editor
44. Breakpoint pauses execution
55. Inspect: e.currentTarget.innerHTML, is the new text there? YES.
66. Inspect: value prop, is it the old value? YES. Bug found!
77. The parent isn't passing the new value back. Check the parent component's onChange handler.

Common Debugging Traps

Trap 1: Reloading the page during debugging. When you refresh, you lose the breakpoint and the state. Instead, modify your code, save, and hot-reload lets you keep the debugger state.

Trap 2: Inspecting the wrong component. React DevTools shows the component tree. If two components have the same name, they're easy to mix up. Use DevTools → Components tab to see which instance is which.

Trap 3: Assuming the error is where it's reported. A race condition might corrupt data on the server, but the error happens later on the client. Trace the data's origin, not just where it died.

Trap 4: Not reading the error. Developers panic and guess. The error message is always a clue. Read it slowly. Google it. Look at the stack trace.


+ 07 / 12

Part 6: Deploy to Vercel

What Deployment Means

Deployment is taking code from your machine, running it on a server, and making it available on the internet.

When you push to main branch (or a deploy branch), Vercel:

  1. 1Clones your repo
  2. 2Installs dependencies (npm install)
  3. 3Builds your app (npm run build)
  4. 4Runs tests (if configured)
  5. 5Deploys to a live URL

If any step fails, the deploy is rejected. You can't ship broken code.

The Deploy Checklist

Before pushing to main, ask:

+ Checklist0 / 7

Environment Variables

Never commit secrets (API keys, database passwords) to Git.

Instead:

  1. 1Create .env.local in your repo root (gitignored).
  2. 2Add secrets there locally.
  3. 3Add the same keys to Vercel's project settings.

Example .env.local:

+ JavaScript
1NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
2NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
3DATABASE_URL=postgresql://...
4STRIPE_SECRET_KEY=sk_live_...

A Postgres connection string packs the username, password, host and database name into a single value. That is exactly why DATABASE_URL must never be committed: one leaked line hands over the whole database.

In your code:

+ TypeScript
1const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
2const stripeKey = process.env.STRIPE_SECRET_KEY; // Server-only

Key rule: Variables prefixed NEXT_PUBLIC_ are visible to the browser (safe for public info). Variables without the prefix are server-only (safe for secrets).

Debugging Deployment Failures

When a deploy fails:

  1. 1Check build logs. Vercel shows a live log of the build. This is where 90% of issues surface.
  2. 2Look for TypeScript errors. The build stops if types don't match.
  3. 3Check environment variables. Is DATABASE_URL missing? Did you set it in Vercel settings?
  4. 4Try building locally. npm run build locally reproduces 99% of deploy issues.

Common failures:

ErrorCauseFix
MODULE_NOT_FOUND: Can't find module 'react'Dependency not in package.jsonnpm install react
TypeError: Cannot read property 'id' of undefinedMissing env varAdd to Vercel settings
Lighthouse score 72Page is slowOptimize images, reduce JS
import type { User } from '@supabase/supabase-js' errorsType mismatch with Supabase typesCheck version in package.json

+ 08 / 12

Part 7: AI-Assisted Development with Verification

The Promise and the Trap

AI is excellent at:

  • Drafting boilerplate (forms, components, API routes)
  • Explaining unfamiliar patterns
  • Generating test cases
  • Refactoring for style
  • Writing documentation

AI is terrible at:

  • Understanding why you chose this approach
  • Catching logic errors (especially off-by-one bugs)
  • Making sure the API shape matches your schema
  • Knowing the codebase conventions
  • Verifying the code actually works

The rule: Always verify.

The Workflow: Draft → Review → Verify

Step 1: Draft

Ask Claude/ChatGPT to write a component:

+ JavaScript
1Write a React component that takes a list of chapters and renders
2them as a sidebar nav. Chapters have id, title, and order. Clicking
3a chapter should call an onSelect callback. Style with Tailwind.

Claude drafts it. You get back working code in 30 seconds.

Step 2: Review (Read the Code)

Before pasting it, read it. Ask:

  • Does it match my codebase style? (e.g., does it use my color tokens, not hardcoded colors?)
  • Are there bugs? (Off-by-one loops? null checks missing?)
  • Does it import the right dependencies? (Is clsx imported if I'm using it?)
  • Does it handle edge cases? (Empty list? No selected chapter?)

Example: Claude writes a loop to map chapters:

+ TypeScript
1{chapters.map((ch, idx) => (
2 <button key={idx} onClick={() => onSelect(ch)}>
3 {ch.title}
4 </button>
5))}

Red flag: Using array index as key is a React anti-pattern. It causes bugs when the list reorders. Fix it:

+ TypeScript
1{chapters.map((ch) => (
2 <button key={ch.id} onClick={() => onSelect(ch)}>
3 {ch.title}
4 </button>
5))}

Step 3: Verify

Paste the code locally. Test it:

+ Terminal
1npm run dev
2# Open the component in your browser
3# Click a chapter → onSelect callback fires? Yes/No
4# Keyboard navigation works? (Can you tab to buttons?)
5# Mobile responsive? (Test on DevTools device emulation)
6# No console errors? (DevTools → Console)

Only after verification do you commit.

When NOT to Use AI

  • When you're learning. Write the code by hand. It's slower but you learn the patterns.
  • When the logic is non-obvious. AI drafts working code, not clever code. Write subtle logic yourself.
  • When your codebase is unique. AI knows common patterns. It doesn't know your style or conventions. Use it for ideas, not verbatim.

Red Flags in AI Output

FlagMeaning
Comments explaining "what" the code doesAI assumes you don't know the language. You know the language. Remove comments.
Generic component names (e.g. Card, Section)AI defaults to boring. Rename to match your domain (e.g. ChapterCard, BookPreview).
No error handlingAI assumes happy path. Add try/catch, null checks, loading states.
No types or loose typingIf your codebase is strict TypeScript, AI might default to looser types. Tighten them.
No testsAI generates code, not confidence. Write tests after.

+ 09 / 12

The Build: Professional Workflow in Practice

Your Challenge

You're going to implement a feature using a professional workflow from start to deploy:

  1. 1Create a branch. Start isolated from main.
  2. 2Write the feature. Use AI to draft, verify by hand.
  3. 3Write tests. 100% test coverage of your component.
  4. 4Create a PR. Write a PR description that explains why this matters.
  5. 5Debug an intentional bug. We'll introduce a bug and you'll track it with DevTools.
  6. 6Deploy. Push to main and watch it ship on Vercel.

The Feature: Chapter Title Validator

Build a component that validates chapter titles in the makeEbook editor.

Requirements:

  • Input: Chapter title (string)
  • Validation: Title must be 1–60 characters
  • Behavior: Show a character counter
  • Behavior: Show red warning if > 60 chars
  • Behavior: Disable save button if invalid
  • Accessibility: Keyboard navigable, screen-reader friendly

Part 1: Create a Branch

+ Terminal
1git checkout -b feature/chapter-title-validator

This creates a new branch. All your work is isolated from main.

Part 2: Draft the Component

Use Claude to draft a ChapterTitleInput component:

+ JavaScript
1Write a React component called ChapterTitleInput.
2Props:
3- title: string (current title)
4- onChange: (title: string) => void
5- onValidationChange: (isValid: boolean) => void
6 
7Requirements:
8- Show an input field for the title
9- Display a character counter below (current / max)
10- If > 60 chars, show red warning "Title exceeds 60 characters"
11- Call onValidationChange with true/false based on validity
12- Style with Tailwind
13- ARIA labels for accessibility
14 
15Return the component code.

Claude gives you the code. Review it (step 2 from the AI workflow above).

Part 3: Test It

Paste it into your editor. Create a test file:

+ TypeScript
1// ChapterTitleInput.test.tsx
2import { render, screen, fireEvent } from '@testing-library/react';
3import { ChapterTitleInput } from './ChapterTitleInput';
4 
5describe('ChapterTitleInput', () => {
6 it('renders with initial title', () => {
7 render(
8 <ChapterTitleInput
9 title="Chapter One"
10 onChange={() => {}}
11 onValidationChange={() => {}}
12 />
13 );
14 expect(screen.getByDisplayValue('Chapter One')).toBeInTheDocument();
15 });
16 
17 it('shows character count', () => {
18 render(
19 <ChapterTitleInput
20 title="Hello"
21 onChange={() => {}}
22 onValidationChange={() => {}}
23 />
24 );
25 expect(screen.getByText(/5 \/ 60/)).toBeInTheDocument();
26 });
27 
28 it('shows warning when title exceeds 60 characters', () => {
29 const longTitle = 'a'.repeat(61);
30 render(
31 <ChapterTitleInput
32 title={longTitle}
33 onChange={() => {}}
34 onValidationChange={() => {}}
35 />
36 );
37 expect(screen.getByText(/exceeds 60 characters/i)).toBeInTheDocument();
38 });
39 
40 it('calls onValidationChange with true when valid', () => {
41 const handleValidationChange = vi.fn();
42 render(
43 <ChapterTitleInput
44 title="Valid Title"
45 onChange={() => {}}
46 onValidationChange={handleValidationChange}
47 />
48 );
49 expect(handleValidationChange).toHaveBeenCalledWith(true);
50 });
51 
52 it('calls onValidationChange with false when invalid', () => {
53 const handleValidationChange = vi.fn();
54 const longTitle = 'a'.repeat(61);
55 render(
56 <ChapterTitleInput
57 title={longTitle}
58 onChange={() => {}}
59 onValidationChange={handleValidationChange}
60 />
61 );
62 expect(handleValidationChange).toHaveBeenCalledWith(false);
63 });
64 
65 it('calls onChange when user types', () => {
66 const handleChange = vi.fn();
67 render(
68 <ChapterTitleInput
69 title=""
70 onChange={handleChange}
71 onValidationChange={() => {}}
72 />
73 );
74 const input = screen.getByRole('textbox');
75 fireEvent.change(input, { target: { value: 'New Title' } });
76 expect(handleChange).toHaveBeenCalledWith('New Title');
77 });
78});

Run tests:

+ Terminal
1npm test

All should pass.

Part 4: Write the PR

+ Terminal
1git add .
2git commit -m "Add ChapterTitleInput component with validation
3 
4- Enforces 60-character limit on chapter titles
5- Shows real-time character counter
6- Calls onValidationChange to gate save button
7- Fully tested with 100% coverage
8"
9 
10git push -u origin feature/chapter-title-validator

Then on GitHub, create a PR with this description:

+ MARKDOWN
1## Summary
2 
3Fixes #XXX: Prevent chapter titles from breaking EPUB layout.
4 
5Authors were creating 200+ character titles that wrapped poorly in
6EPUB readers. This component enforces a 60-character limit with a
7visual counter, letting authors make informed edits.
8 
9## Why This Way?
10 
11- **Why not truncate?** Silently truncating loses meaning. A counter
12 lets the author decide what to cut.
13- **Why 60?** EPUB best practices recommend ≤80 chars. 60 gives
14 breathing room on mobile readers.
15- **Why onValidationChange callback?** The parent can disable the
16 save button while the title is invalid. Prevents shipping
17 incomplete chapters.
18 
19## Testing
20 
211. Open a book
222. Go to any chapter
233. Click the title input
244. Type 61+ characters
255. Confirm:
26 - Red warning appears
27 - Character counter shows "61 / 60"
28 - Save button is disabled
296. Delete one character
307. Confirm:
31 - Warning disappears
32 - Save button is enabled
33 
34Run `npm test` and confirm every unit test is green.

Part 5: Debug an Intentional Bug

We'll introduce a subtle bug: the counter shows length / 59 instead of length / 60.

Your job:

  1. 1Notice the off-by-one in the UI
  2. 2Open DevTools → Elements
  3. 3Inspect the counter text
  4. 4Open DevTools → Sources
  5. 5Set a breakpoint in the render
  6. 6Step through and find where maxLength is defined
  7. 7Fix it

This teaches you the debugging workflow.

Part 6: Merge and Deploy

Once tests pass and the PR is reviewed:

+ Terminal
1git checkout main
2git pull origin main
3git merge feature/chapter-title-validator
4git push origin main

Vercel automatically deploys. In 2–3 minutes, your feature is live.

Go to the app. Test it on production.

Checkpoint

You should be able to:

+ Checklist0 / 7

+ 10 / 12

Common Gotchas

Git

"I committed to main by accident." No problem. Create a branch from the current state:

+ Terminal
1git branch feature/oops
2git reset --hard origin/main # Revert main
3git checkout feature/oops # Switch to your branch

Your commit is safe on the branch.

"I want to undo my last commit."

+ Terminal
1git reset --soft HEAD~1 # Undo commit, keep changes
2git reset --hard HEAD~1 # Undo commit, discard changes

"merge conflict" When two branches edit the same lines, Git can't auto-merge. You choose which version to keep:

+ Terminal
1# Open the conflicted file. It looks like:
2<<<<<<< HEAD
3 your version
4=======
5 their version
6>>>>>>> feature/other-branch

Delete the markers and keep the version you want. Then commit the resolution.

Testing

"Test passes locally but fails in CI." CI runs in a clean environment. Local issues:

  • Uncommitted changes
  • Local env vars not mirrored in CI config
  • Different Node version
  • Flaky test (passes 90% of the time)

Run npm test multiple times locally. If it's flaky, the test is the problem, not your code.

"I can't figure out why the test fails." Add debugging:

+ TypeScript
1console.log("value:", value);
2console.log("component:", screen.debug());

screen.debug() prints the entire rendered DOM. This shows what the test actually sees.

Deployment

"Deployment succeeded but the app doesn't work."

  1. 1Check browser console (DevTools → Console)
  2. 2Check Vercel logs (Vercel dashboard → Deployments → Logs)
  3. 3Check environment variables match Vercel settings

"The build failed but the error message is cryptic." Read the full error in Vercel's build log. Scroll up. The first error is usually the real one. Subsequent errors are cascades.


+ 11 / 12

Further Reading

  • Git internals: Read "Pro Git" chapters 1–3 (free online)
  • Testing React: React Testing Library docs (https://testing-library.com/react)
  • Debugging: Chrome DevTools docs (https://developer.chrome.com/docs/devtools/)
  • PRs: Read 10 well-written PRs on GitHub (search is:pr is:merged to find merged PRs)

+ 12 / 12

Summary

Professional development is about collaboration, clarity, and confidence.

  • Git is your narrative. Write commit messages for yourself-in-six-months.
  • PRs are asynchronous communication. Explain why before explaining what.
  • Code reading is a skill. Use the three-level technique: topography, seams, trace an action.
  • Tests are confidence. A good test suite lets you refactor without fear.
  • Debugging is methodical. Error location is a hint. Start there.
  • Deployment is ceremony. Follow the checklist every time.
  • AI is a tool. Draft fast, verify thoroughly. Never trust output without reading it.

The rest of your career is reading code, fixing bugs, and shipping features. Master these skills and you're set.

+ Up nextCapstonePreviouslyAuth: Gating Pages Behind Login