Skip to main content

πŸ”€ Pull Requests and Code Reviews

A pull request is more than a merge button. It's a proposal, a discussion thread, a quality checkpoint, and a permanent record of why a change was made β€” all in one. This lesson shows you how to author pull requests that are easy to review and how to review others' code thoughtfully and kindly.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Describe the anatomy and lifecycle of a pull request from draft to merge
  • Author small, focused PRs with descriptions that explain the what, why, and how
  • Conduct a structured code review covering correctness, design, security, and tests
  • Give constructive feedback that improves the code without bruising the author
  • Wire up automated checks so routine issues are caught before a human looks

Estimated Time: 35–45 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Open a real pull request and perform a structured review of a peer's change.

In This Lesson

Why Pull Requests Exist

A pull request (PR) asks a project to pull the changes on your branch into another branch β€” usually main. But its real value is everything that happens before the merge: teammates read the diff, ask questions, catch bugs, and leave a paper trail future developers can read.

πŸ’‘ A useful analogy: A pull request is like submitting an essay for peer review before publication. You don't just slide your draft onto the printing press β€” you circulate it, colleagues mark it up, you revise, and only the approved version goes out. The comments left behind also explain the reasoning long after the ink dries.

Pull requests give a team four things at once: quality control (a second set of eyes), knowledge sharing (reviewers learn the codebase), mentorship (juniors learn from seniors), and documentation (the discussion is preserved forever).

Anatomy & Lifecycle

Every PR compares a source branch (your changes, also called head) against a target branch (where you want them to land, also called base). Around that core it gathers a title, a description, the list of commits, the changed files, review comments, status checks, labels, and linked issues.

The lifecycle

A PR moves through a predictable set of states. Understanding them tells you exactly what needs to happen next:

stateDiagram-v2 [*] --> Draft: Open as draft [*] --> Open: Open ready for review Draft --> Open: Mark ready for review Open --> InReview: Reviewer starts InReview --> ChangesRequested: Request changes ChangesRequested --> Open: Author pushes fixes InReview --> Approved: Approve Approved --> Merged: Merge Open --> Closed: Close without merging Merged --> [*] Closed --> [*]
  1. Creation β€” opened as a draft (work in progress) or ready for review.
  2. Review β€” reviewers read the code and leave feedback.
  3. Revision β€” the author addresses feedback with new commits.
  4. Approval β€” reviewers approve once satisfied.
  5. Merge β€” the change lands in the target branch.
  6. Cleanup β€” the source branch is deleted.

πŸ“– Draft pull requests

A draft PR is explicitly marked "not ready to merge." Use it to get early feedback on your direction, to kick off CI checks while you keep working, and to reserve the PR number β€” all without the risk of an accidental merge.

Authoring a Great PR

The reviewer's job is only as easy as you make it. Two habits matter more than anything else: keep PRs small and write a description that answers questions before they're asked.

Prepare before you open it

# Bring your branch up to date with the target
git checkout main
git pull
git checkout feature-branch
git rebase main

# Run the tests and linters locally first
npm test
npm run lint

# Review your own diff before anyone else does
git diff main...feature-branch

Keep it small and focused

A PR should do one thing. Research and hard experience agree that review quality falls off a cliff past a few hundred lines of change β€” reviewers skim, and bugs slip through. When a feature is large, split it into a sequence of smaller PRs that build on each other.

flowchart TD A[User management feature] --> B[PR 1: Database schema] A --> C[PR 2: Backend API] A --> D[PR 3: Authentication] A --> E[PR 4: Frontend UI] B -.-> C C -.-> D D -.-> E

Write a description that helps

A good description covers what changed, why it was needed, and how you approached it, then notes any testing and risks. Standardize it with a template at .github/PULL_REQUEST_TEMPLATE.md:

## What & why
Brief summary of the change and the problem it solves.

## Related issue
Closes #42

## How it works
Key implementation decisions a reviewer should know about.

## Testing done
- Added unit tests for the new validation path
- Manually verified the happy path and empty-input case

## Checklist
- [ ] Tests added / updated
- [ ] Docs updated if needed
- [ ] No new warnings

⚠️ Separate refactors from features

Mixing a big refactor with a new feature in one PR is a classic mistake β€” the reviewer can't tell which diff lines change behavior and which just move code around. Land the refactor first, then build the feature on top.

Reviewing Code Well

A good review is systematic, not a random scroll through the diff. Work from the outside in: understand the context, grasp the big picture, then examine details.

  1. Read the description and any linked issue β€” know what the change is trying to do.
  2. Scan the overall shape β€” does the architecture fit the codebase before you nitpick lines?
  3. Read line by line for correctness, edge cases, and clarity.
  4. Check the tests β€” do they cover the change, including error paths?
  5. Confirm the docs reflect any user-facing change.
  6. Summarize your findings and choose an outcome.

What to look for

Prioritize in this order β€” correctness and security matter far more than a misplaced space:

PriorityQuestion to ask
1. CorrectnessDoes it do what it claims, including edge cases?
2. SecurityIs user input validated? Any injected secrets or new vulnerabilities?
3. DesignIs it well-structured and maintainable? Any needless complexity?
4. TestsIs there adequate, meaningful coverage?
5. ReadabilityAre names clear? Would a newcomer follow it?
6. StyleDoes it match project conventions? (Best left to linters.)

GitHub's review outcomes

When you click Review changes, you choose one of three verdicts:

  • Comment β€” general feedback, no explicit verdict.
  • Approve β€” you're happy for this to merge.
  • Request changes β€” something must be fixed before merging.

Use the suggestion feature (the pencil icon on a line comment) to propose exact replacement code; the author can accept it with one click.

Constructive Feedback

The tone of your feedback decides whether it improves the code or starts a fight. The golden rule: review the code, not the coder. Assume good intent, be specific, and explain the why behind each suggestion.

Instead of…Try…
"This code is a mess.""This might read more clearly if we extracted it into a helper β€” what do you think?"
"Why would you do it this way?""I'm curious about the reasoning behind this approach."
"You forgot the empty case.""It looks like we'd hit an error when the input is empty β€” should we guard against that?"
"This is wrong.""I think there may be a bug here because the counter never resets."

A well-structured review balances praise with suggestions and separates must-fix from nice-to-have:

### Strengths
- Token generation is clean and well tested.
- Nice job isolating the auth logic in its own middleware.

### Suggestions (blocking)
- The login route should rate-limit attempts to prevent brute forcing.

### Nits (non-blocking)
- Consider renaming `d` to `expiresAt` for clarity.

### Question
- Why the 24-hour token lifetime? Would a shorter one hurt UX much?

πŸ’‘ Mentor through reviews

A review is a teaching moment. When you suggest a pattern β€” say, replacing nested ifs with early returns β€” show a tiny before/after and link to a reference. The author learns the principle, not just the one-off fix.

// Instead of deeply nested conditionals:
function processUser(user) {
  if (user) {
    if (user.isActive) {
      if (user.hasPermission) {
        return true;
      }
    }
  }
  return false;
}

// Prefer early returns β€” the "happy path" reads top to bottom:
function processUser(user) {
  if (!user) return false;
  if (!user.isActive) return false;
  if (!user.hasPermission) return false;
  return true;
}

Automating the Boring Parts

Humans should spend their attention on design and logic, not on catching a missing semicolon. Let automated status checks handle the mechanical review so people can focus on judgment.

Run checks on every PR

A GitHub Actions workflow can lint, test, and scan every pull request automatically:

name: Code Quality Checks

on:
  pull_request:
    branches: [ main ]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test

Then make those checks required: in Settings β†’ Branches, add a branch-protection rule for main and enable "Require status checks to pass before merging." Now a PR simply cannot merge while tests are red.

πŸ“– Helpful review bots

Dependabot opens PRs to update vulnerable dependencies. Codecov reports how a change affects test coverage. CodeQL (GitHub's built-in scanner) flags security issues. Each handles a slice of review no human should have to do by hand.

βœ… The right balance

Automate the mundane β€” formatting, style, obvious bugs β€” and reserve human review for the things machines can't judge: is this the right design? does it solve the real problem? Automation enhances human review; it never replaces it.

Hands-on Exercise

πŸ‹οΈ Open a PR and review one

Objective: Experience both sides of the workflow β€” authoring a reviewable PR and performing a structured review.

Instructions:

  1. In a shared or practice repository, create a branch and make one small, focused change:
    git checkout -b feature/add-greeting
    # edit a file...
    git add .
    git commit -m "Add greeting helper with tests"
    git push -u origin feature/add-greeting
  2. On GitHub, click Compare & pull request. Write a description covering what, why, and how, and link an issue with Closes #N.
  3. Ask a peer (or a second account) to review it. As the reviewer, work through the six-step process: context β†’ big picture β†’ line by line β†’ tests β†’ docs β†’ summary.
  4. Leave at least one line comment and one suggestion. Phrase both as questions or team-framed suggestions, not commands.
  5. As the author, address the feedback with a follow-up commit, resolve the threads, and request a re-review. Merge once approved, then delete the branch.
πŸ’‘ Hint

Keep the change genuinely small β€” a single function plus its test is ideal. The point is to practice the process, and a tiny diff lets you focus on writing a clean description and giving thoughtful feedback rather than wrestling with a huge review.

βœ… What success looks like

The PR has a description that answers a reviewer's questions up front, at least one resolved review comment, a follow-up commit that addresses feedback, an "Approved" review, and a clean merge that auto-closes the linked issue. The feedback you gave reads as helpful, not harsh.

🎯 Quick Quiz

Question 1: Why should pull requests be kept small and focused?

Question 2: When giving review feedback, which approach is most constructive?

Question 3: What is the best division of labor between automated checks and human reviewers?

Best Practices

βœ… Do

  • Rebase on the target branch and run tests before opening the PR.
  • Write a description that explains what, why, and how β€” link the related issue.
  • Review promptly (aim for within one business day) so work keeps flowing.
  • Praise good work; acknowledge improvements over time.

⚠️ Don't

  • Don't bundle unrelated fixes or a big refactor into a feature PR.
  • Don't rubber-stamp "LGTM" without actually reading the diff and tests.
  • Don't let disagreements fester in comment threads β€” move complex debates to a quick call.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A pull request is a proposal plus a discussion plus a quality checkpoint plus a permanent record.
  • PRs move through a predictable lifecycle: draft β†’ open β†’ review β†’ revise β†’ approve β†’ merge.
  • Keep PRs small and focused and write descriptions that answer questions before they're asked.
  • Review systematically and prioritize correctness and security over style.
  • Give feedback that reviews the code, not the coder, and let automation handle the mundane checks.

πŸ“š Further Reading

πŸš€ What's Next?

You've seen automated checks running on a pull request. Next we'll go under the hood of that automation and learn to build workflows from scratch with GitHub Actions.

πŸŽ‰ Great work!

Reviewing well is a superpower β€” it makes the whole team better, one PR at a time.