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
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.
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:
main (the production-ready universe) ↳ your-feature-branch (your sandbox, isolated, safe) └─ commit 1: add button component └─ commit 2: add tests for button └─ commit 3: integrate into headerWhen 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:
fix stuffWIPlolRevert "fix stuff"actually fixed it this timeExample good commit history:
Add TextInput component with label and error stateAdd unit tests for TextInput keyboard handlingFix TextInput focus loss on parent re-renderIntegrate TextInput into LoginFormEach commit stands alone. Each one is "done."
The Workflow: Branch → Commit → PR → Merge
graph LR A["git checkout -b your-feature"] B["Write code, test locally"] C["git add, git commit"] D["git push -u origin your-feature"] E["Create PR on GitHub"] F["Code review, feedback"] G["Address feedback, commit again"] H["Merge PR into main"] A --> B --> C --> D --> E --> F --> G --> F G -->|approved| HPart 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
## Summary Fixes #742: Add character limit validation to chapter titles. Writers were creating 200-character chapter titles that broke EPUBformatting. This change enforces a 60-character limit with a visualcounter. ## Why This Way? - **Why not just truncate?** Truncation silently mangles meaning. A warning + counter lets the author decide.- **Why 60 characters?** EPUB specs recommend ≤80; we chose 60 for breathing room in mobile readers.- **Why not server-side only?** Client feedback is instant. No round-trip. ## Tradeoffs - **Pro:** Real-time feedback, compliant with EPUB, users keep control.- **Con:** One more input validation rule to maintain. ## How to Test 1. Open a book in the editor2. Click any chapter title3. Type 61+ characters4. Confirm you see the red warning and a character count5. Try to submit the form, it should be disabled6. Delete one character (now at 60)7. Confirm the warning disappears and form is enabled ## Checklist - [x] Tests pass locally- [x] No console warnings- [x] Mobile viewport works (tested on iPhone SE)- [x] Lighthouse score 90+The Three Questions Every PR Must Answer
- 1What did you change? (Be specific. "Bug fixes" is noise. "Fixed race condition in book sync" is useful.)
- 2Why did you change it? (Business value, user benefit, or technical debt. Connect to a user problem or issue number.)
- 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.
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)
1. It's a React component. 'use client' at the top means it runs in the browser.2. Roughly 1,000 lines (big).3. Lots of helper functions and event handlers.4. Props interface: `RichTextEditorProps` extends HTMLAttributes.5. 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
contentEditablediv (the actual editor) - Event listeners for keyboard shortcuts
Data flow:
User types → onChange fires → parent updates state → new value passes back down → editor re-rendersLevel 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
1. User clicks the "B" (bold) button2. onClick handler fires: `execCommand('bold')`3. Browser's contentEditable API toggles <strong> tags4. onInput fires (browser event)5. onChange callback fires with new HTML6. Parent receives new HTML, updates state7. Component re-renders with new value8. Toolbar button sees new selection, updates active stateEach 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.
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
👨💻 E2E Tests (run the whole app, click buttons, verify UI) /\ / \ 10% of tests, 50% of coverage / \ /______\ 🧪 Integration Tests (test one feature, multiple components) / \ / \ 30% of tests, 40% of coverage / \ /____________\ 🔧 Unit Tests (test one function, one behavior) / \ / \ 60% of tests, 10% of coverage / \ /____________________\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.
// Button.tsxinterface ButtonProps { label: string; onClick?: () => void; disabled?: boolean;} export function Button({ label, onClick, disabled }: ButtonProps) { return ( <button onClick={onClick} disabled={disabled} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50" > {label} </button> );}Now test it:
// Button.test.tsximport { render, screen, fireEvent } from '@testing-library/react';import { Button } from './Button'; describe('Button', () => { it('renders with label', () => { render(<Button label="Click me" />); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('calls onClick when clicked', () => { const handleClick = vi.fn(); // Mock function render(<Button label="Click me" onClick={handleClick} />); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledOnce(); }); it('does not call onClick when disabled', () => { const handleClick = vi.fn(); render(<Button label="Click me" onClick={handleClick} disabled />); fireEvent.click(screen.getByText('Click me')); expect(handleClick).not.toHaveBeenCalled(); }); it('disables the button when disabled prop is true', () => { render(<Button label="Click me" disabled />); expect(screen.getByText('Click me')).toBeDisabled(); });});What each test does:
- 1Renders with label, The component accepts a label and displays it. This is the happy path.
- 2Calls onClick when clicked, User interaction works. Uses
vi.fn()to spy on the callback. - 3Does not call onClick when disabled, Edge case. Disabled buttons shouldn't fire callbacks even if clicked.
- 4Disables the button, Another edge case. The
disabledattribute 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.
// TextInput.tsxinterface TextInputProps { label: string; value: string; onChange: (value: string) => void; maxLength?: number; error?: string;} export function TextInput({ label, value, onChange, maxLength, error}: TextInputProps) { const isAtLimit = maxLength && value.length >= maxLength; return ( <div className="flex flex-col gap-2"> <label className="text-sm font-medium">{label}</label> <input type="text" value={value} onChange={(e) => onChange(e.target.value)} maxLength={maxLength} className={`px-3 py-2 border rounded ${ error ? 'border-red-500' : 'border-gray-300' } ${isAtLimit ? 'border-orange-500' : ''}`} /> {isAtLimit && ( <p className="text-xs text-orange-600"> {value.length} / {maxLength} characters </p> )} {error && <p className="text-xs text-red-600">{error}</p>} </div> );}Tests:
// TextInput.test.tsximport { render, screen, fireEvent } from '@testing-library/react';import { TextInput } from './TextInput'; describe('TextInput', () => { it('renders label and input', () => { render( <TextInput label="Email" value="" onChange={() => {}} /> ); expect(screen.getByLabelText('Email')).toBeInTheDocument(); }); it('updates value on user input', () => { const handleChange = vi.fn(); render( <TextInput label="Email" value="" onChange={handleChange} /> ); const input = screen.getByLabelText('Email'); fireEvent.change(input, { target: { value: 'test@example.com' } }); expect(handleChange).toHaveBeenCalledWith('test@example.com'); }); it('shows character count at max length', () => { render( <TextInput label="Title" value="Hello" onChange={() => {}} maxLength={10} /> ); expect(screen.getByText('5 / 10 characters')).toBeInTheDocument(); }); it('displays error message', () => { render( <TextInput label="Email" value="" onChange={() => {}} error="Invalid email" /> ); expect(screen.getByText('Invalid email')).toBeInTheDocument(); }); it('applies error styling when error prop exists', () => { render( <TextInput label="Email" value="" onChange={() => {}} error="Invalid email" /> ); const input = screen.getByLabelText('Email'); expect(input).toHaveClass('border-red-500'); });});Key patterns:
- Use
screen.getByText()to find elements by human-readable text, notquerySelector. - Use
fireEventto 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:
{ "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest --coverage" }}Run:
npm test # Watch mode, re-runs on savenpm run test:ui # Visual test dashboardnpm run test:coverage # Show code coverage %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:
TypeError: Cannot read property 'chapters' of null at getChapterCount (book.ts:45:12) at renderEditor (page.tsx:120:5)Translation:
- Line 45 of book.ts:
chaptersisnullwhen 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
- 1Read the error. The stack trace is the roadmap.
- 2Reproduce it. Can you make it happen again? How?
- 3Inspect at the failure point. Set a breakpoint right before the error. Watch the variables.
- 4Trace backwards. How did the data get into this bad state?
- 5Fix the root cause. Not the symptom. The root.
Using Browser DevTools
Chrome / Firefox DevTools:
Open DevTools (F12). Go to Sources tab.
- 1Set a breakpoint. Click the line number where you want to pause.
- 2Trigger the bug. Do the action that breaks it.
- 3Step through. Use the step buttons: - Step Over (→): Execute one line - Step Into (↓): Enter a function call - Step Out (↑): Exit the current function
- 4Inspect variables. Hover over them or type in the Console
- 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.
1. Open DevTools → Sources2. Set breakpoint on the onChange handler (line ~120)3. Type in the editor4. Breakpoint pauses execution5. Inspect: e.currentTarget.innerHTML, is the new text there? YES.6. Inspect: value prop, is it the old value? YES. Bug found!7. 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.
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:
- 1Clones your repo
- 2Installs dependencies (
npm install) - 3Builds your app (
npm run build) - 4Runs tests (if configured)
- 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:
Environment Variables
Never commit secrets (API keys, database passwords) to Git.
Instead:
- 1Create
.env.localin your repo root (gitignored). - 2Add secrets there locally.
- 3Add the same keys to Vercel's project settings.
Example .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.coNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...DATABASE_URL=postgresql://...STRIPE_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:
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;const stripeKey = process.env.STRIPE_SECRET_KEY; // Server-onlyKey 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:
- 1Check build logs. Vercel shows a live log of the build. This is where 90% of issues surface.
- 2Look for TypeScript errors. The build stops if types don't match.
- 3Check environment variables. Is
DATABASE_URLmissing? Did you set it in Vercel settings? - 4Try building locally.
npm run buildlocally reproduces 99% of deploy issues.
Common failures:
| Error | Cause | Fix |
|---|---|---|
MODULE_NOT_FOUND: Can't find module 'react' | Dependency not in package.json | npm install react |
TypeError: Cannot read property 'id' of undefined | Missing env var | Add to Vercel settings |
Lighthouse score 72 | Page is slow | Optimize images, reduce JS |
import type { User } from '@supabase/supabase-js' errors | Type mismatch with Supabase types | Check version in package.json |
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:
Write a React component that takes a list of chapters and rendersthem as a sidebar nav. Chapters have id, title, and order. Clickinga 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
clsximported if I'm using it?) - Does it handle edge cases? (Empty list? No selected chapter?)
Example: Claude writes a loop to map chapters:
{chapters.map((ch, idx) => ( <button key={idx} onClick={() => onSelect(ch)}> {ch.title} </button>))}Red flag: Using array index as key is a React anti-pattern. It causes bugs when the list reorders. Fix it:
{chapters.map((ch) => ( <button key={ch.id} onClick={() => onSelect(ch)}> {ch.title} </button>))}Step 3: Verify
Paste the code locally. Test it:
npm run dev# Open the component in your browser# Click a chapter → onSelect callback fires? Yes/No# Keyboard navigation works? (Can you tab to buttons?)# Mobile responsive? (Test on DevTools device emulation)# 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
| Flag | Meaning |
|---|---|
| Comments explaining "what" the code does | AI 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 handling | AI assumes happy path. Add try/catch, null checks, loading states. |
| No types or loose typing | If your codebase is strict TypeScript, AI might default to looser types. Tighten them. |
| No tests | AI generates code, not confidence. Write tests after. |
The Build: Professional Workflow in Practice
Your Challenge
You're going to implement a feature using a professional workflow from start to deploy:
- 1Create a branch. Start isolated from main.
- 2Write the feature. Use AI to draft, verify by hand.
- 3Write tests. 100% test coverage of your component.
- 4Create a PR. Write a PR description that explains why this matters.
- 5Debug an intentional bug. We'll introduce a bug and you'll track it with DevTools.
- 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
git checkout -b feature/chapter-title-validatorThis creates a new branch. All your work is isolated from main.
Part 2: Draft the Component
Use Claude to draft a ChapterTitleInput component:
Write a React component called ChapterTitleInput.Props:- title: string (current title)- onChange: (title: string) => void- onValidationChange: (isValid: boolean) => void Requirements:- Show an input field for the title- Display a character counter below (current / max)- If > 60 chars, show red warning "Title exceeds 60 characters"- Call onValidationChange with true/false based on validity- Style with Tailwind- ARIA labels for accessibility Return 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:
// ChapterTitleInput.test.tsximport { render, screen, fireEvent } from '@testing-library/react';import { ChapterTitleInput } from './ChapterTitleInput'; describe('ChapterTitleInput', () => { it('renders with initial title', () => { render( <ChapterTitleInput title="Chapter One" onChange={() => {}} onValidationChange={() => {}} /> ); expect(screen.getByDisplayValue('Chapter One')).toBeInTheDocument(); }); it('shows character count', () => { render( <ChapterTitleInput title="Hello" onChange={() => {}} onValidationChange={() => {}} /> ); expect(screen.getByText(/5 \/ 60/)).toBeInTheDocument(); }); it('shows warning when title exceeds 60 characters', () => { const longTitle = 'a'.repeat(61); render( <ChapterTitleInput title={longTitle} onChange={() => {}} onValidationChange={() => {}} /> ); expect(screen.getByText(/exceeds 60 characters/i)).toBeInTheDocument(); }); it('calls onValidationChange with true when valid', () => { const handleValidationChange = vi.fn(); render( <ChapterTitleInput title="Valid Title" onChange={() => {}} onValidationChange={handleValidationChange} /> ); expect(handleValidationChange).toHaveBeenCalledWith(true); }); it('calls onValidationChange with false when invalid', () => { const handleValidationChange = vi.fn(); const longTitle = 'a'.repeat(61); render( <ChapterTitleInput title={longTitle} onChange={() => {}} onValidationChange={handleValidationChange} /> ); expect(handleValidationChange).toHaveBeenCalledWith(false); }); it('calls onChange when user types', () => { const handleChange = vi.fn(); render( <ChapterTitleInput title="" onChange={handleChange} onValidationChange={() => {}} /> ); const input = screen.getByRole('textbox'); fireEvent.change(input, { target: { value: 'New Title' } }); expect(handleChange).toHaveBeenCalledWith('New Title'); });});Run tests:
npm testAll should pass.
Part 4: Write the PR
git add .git commit -m "Add ChapterTitleInput component with validation - Enforces 60-character limit on chapter titles- Shows real-time character counter- Calls onValidationChange to gate save button- Fully tested with 100% coverage" git push -u origin feature/chapter-title-validatorThen on GitHub, create a PR with this description:
## Summary Fixes #XXX: Prevent chapter titles from breaking EPUB layout. Authors were creating 200+ character titles that wrapped poorly inEPUB readers. This component enforces a 60-character limit with avisual counter, letting authors make informed edits. ## Why This Way? - **Why not truncate?** Silently truncating loses meaning. A counter lets the author decide what to cut.- **Why 60?** EPUB best practices recommend ≤80 chars. 60 gives breathing room on mobile readers.- **Why onValidationChange callback?** The parent can disable the save button while the title is invalid. Prevents shipping incomplete chapters. ## Testing 1. Open a book2. Go to any chapter3. Click the title input4. Type 61+ characters5. Confirm: - Red warning appears - Character counter shows "61 / 60" - Save button is disabled6. Delete one character7. Confirm: - Warning disappears - Save button is enabled Run `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:
- 1Notice the off-by-one in the UI
- 2Open DevTools → Elements
- 3Inspect the counter text
- 4Open DevTools → Sources
- 5Set a breakpoint in the render
- 6Step through and find where
maxLengthis defined - 7Fix it
This teaches you the debugging workflow.
Part 6: Merge and Deploy
Once tests pass and the PR is reviewed:
git checkout maingit pull origin maingit merge feature/chapter-title-validatorgit push origin mainVercel automatically deploys. In 2–3 minutes, your feature is live.
Go to the app. Test it on production.
Checkpoint
You should be able to:
Common Gotchas
Git
"I committed to main by accident." No problem. Create a branch from the current state:
git branch feature/oopsgit reset --hard origin/main # Revert maingit checkout feature/oops # Switch to your branchYour commit is safe on the branch.
"I want to undo my last commit."
git reset --soft HEAD~1 # Undo commit, keep changesgit 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:
# Open the conflicted file. It looks like:<<<<<<< HEAD your version======= their version>>>>>>> feature/other-branchDelete 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:
console.log("value:", value);console.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."
- 1Check browser console (DevTools → Console)
- 2Check Vercel logs (Vercel dashboard → Deployments → Logs)
- 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.
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:mergedto find merged PRs)
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.