Multi-File Projects
Learning Outcomes
- Direct Claude to work across multiple interconnected files
- Understand how Claude navigates and discovers code
- Perform refactoring that spans file boundaries
- Add features that require changes in multiple layers
- Scaffold entire modules from a description
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 3 min | Why multi-file work is the real power |
| Demo | 12 min | Adding a feature across 4 files |
| Explain | 7 min | How Claude discovers and navigates code |
| Demo | 10 min | Cross-file refactoring |
| Demo | 8 min | Scaffolding a new module |
| Wrap-up | 5 min | Key takeaways |
Before You Begin
Pre-work:
- Complete Lesson 3
- Have a project with multiple interconnected files (e.g., a web app with frontend + API + database layers)
Shopping List:
- Claude Code installed
- A multi-file project (at least 10+ files across different directories)
- Git initialised with a clean working state
Claude doesn't read your entire project into memory at once. It navigates intelligently:
- Scans the file tree — knows what files exist
- Reads on demand — opens files as relevant to your question
- Follows imports — traces dependencies between files
- Uses grep/search — finds references across the codebase
When you ask Claude to do something, it:
- Identifies which files need to change
- Reads those files (and their dependencies)
- Proposes changes across all affected files
This matters because it means you can work with projects that are far too large to fit in any context window. Claude only loads what it needs, when it needs it — similar to how a human developer navigates a codebase with an IDE rather than reading every file front-to-back.
You can see this navigation in action:
> Add a "created_at" timestamp to all API responses
Claude will:
- Search for API route files
- Read each one to understand the response format
- Identify where responses are constructed
- Propose changes to each file that sends a response
What this looks like in your terminal:
Claude: I'll search for files that construct API responses.
[Read] src/api/users.ts
[Read] src/api/orders.ts
[Read] src/api/products.ts
[Search] grep -r "res.json" src/api/
I can see 3 route files that return JSON responses. Each uses
a slightly different format. I'll add created_at consistently:
A real feature typically touches multiple layers. Here's how to direct Claude:
The request:
> Add a "favourite items" feature:
> - Database: add a favourites junction table (user_id, item_id, created_at)
> - API: POST /api/favourites/:itemId to toggle, GET /api/favourites to list
> - Frontend: heart icon on ItemCard that toggles favourite state
> - Use optimistic UI updates on the frontend
Notice the prompt structure: it tells Claude exactly which layers are involved and what each layer needs to do. This is important because it prevents Claude from making architectural decisions for you — decisions like "where should the route file live?" and "should I use a new hook or modify an existing one?" are yours to make.
Claude's approach:
- Creates the database migration
- Creates or modifies the API route file
- Modifies the frontend component
- May create a custom hook for the favourite state
Guide it if needed:
> The API routes should go in src/routes/favourites.ts
> The database migration should use our Prisma schema
> The heart component already exists at src/components/icons/Heart.tsx
These hints save Claude from guessing. Without them, Claude might create a new heart icon when one already exists, or put routes in the wrong directory. The more established your project patterns are, the less guidance Claude needs — especially if those patterns are documented in CLAUDE.md.
Review strategy for multi-file changes:
- Check each file individually (Claude shows them one by one)
- Verify the connections between files (does the frontend call the right endpoint?)
- Confirm the types match across layers
- Check that error cases are handled at each boundary
Example — verifying cross-layer consistency:
> Before I approve these changes, verify:
> 1. The frontend calls the exact endpoint paths you defined in the API
> 2. The types used in the frontend match the API response shape
> 3. Error responses from the API are handled in the frontend
Refactoring across files is where Claude saves the most time:
Rename with implications:
> Rename the UserService class to ProfileService. Update all imports,
> references, variable names, and file names across the project.
Extract shared code:
> The date formatting logic is duplicated in Dashboard.tsx,
> UserProfile.tsx, and ActivityFeed.tsx. Extract it into a shared
> utility function in src/utils/dates.ts and update all three files.
Change a pattern everywhere:
> Convert all fetch() calls in src/api/ to use our new httpClient
> wrapper from src/lib/http.ts. It handles auth headers and error
> formatting automatically.
Migrate interfaces:
> We're changing our API response format from { result, error } to
> { data, error, meta }. Update all API handlers and all frontend
> code that reads responses.
These operations would take hours manually. Claude handles them in minutes because it can:
- Find every reference programmatically
- Apply the change consistently
- Maintain type safety across the refactor
A real refactoring session:
Suppose you have date formatting scattered across your app:
> The date formatting logic is duplicated in Dashboard.tsx,
> UserProfile.tsx, and ActivityFeed.tsx. Extract it into a shared
> utility function in src/utils/dates.ts and update all three files.
Claude's output will look something like:
Created src/utils/dates.ts:
export function formatRelativeDate(date: Date | string): string {
const d = new Date(date);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
// ... full implementation
}
Updated Dashboard.tsx:
- Removed inline formatDate function (lines 12-25)
+ Added import { formatRelativeDate } from '../utils/dates'
Updated UserProfile.tsx:
- Removed formatTimestamp function (lines 8-18)
+ Added import { formatRelativeDate } from '../utils/dates'
Updated ActivityFeed.tsx:
- Removed duplicated date logic (lines 31-42)
+ Added import { formatRelativeDate } from '../../utils/dates'
The key value here isn't just saving typing — it's that Claude ensures all three call sites use the same function signature and that the extracted function covers all the edge cases that the three separate implementations handled differently.
Creating an entirely new feature module from scratch:
> Scaffold a complete "notifications" module following the same
> patterns as our existing "messages" module:
>
> Needed:
> - src/types/notification.ts (types)
> - src/api/notifications.ts (CRUD endpoints)
> - src/hooks/useNotifications.ts (React hook)
> - src/components/notifications/NotificationList.tsx
> - src/components/notifications/NotificationItem.tsx
> - src/components/notifications/NotificationBadge.tsx
> - prisma/migrations/[timestamp]_add_notifications.sql
>
> The notification should have: id, userId, type (info|warning|error),
> title, body, read (boolean), createdAt.
The "follow existing patterns" instruction is key. By pointing to an existing module, Claude will:
- Match the file structure
- Use the same coding style
- Follow the same state management approach
- Use consistent error handling
Why list the files explicitly? You might think Claude could figure out the file structure. It often can — but listing the files gives you two advantages:
- You control the architecture (where files go, how many components)
- You get exactly the deliverables you need to review (nothing extra, nothing missing)
If you don't list files, Claude might create 4 files or 12 files depending on its judgement. Being explicit makes the output predictable and reviewable.
The result:
After running this prompt, you'll have 7 new files that all follow the same conventions as your existing messages module. The types will match your Prisma schema patterns, the hook will use the same fetching library, and the components will use the same state management approach. Consistency without manual effort.
When multi-file changes get complex, use these strategies:
Strategy 1: Types first
> First, create the TypeScript types for the entire feature in
> src/types/. Don't implement anything yet — just the interfaces.
Review and approve. Then:
> Now implement the API layer using those types.
Strategy 2: One layer at a time
> Let's add the database schema first. Just the migration, nothing else.
Approve. Then:
> Now add the API endpoints that use this schema.
Strategy 3: Plan then execute
> I want to add user roles and permissions. Before making changes,
> tell me which files you'd modify and what changes you'd make to each.
Review the plan. Then:
> That plan looks good. Proceed with the implementation.
When to use which:
- Types first: when you want to validate the data model before building
- Layer by layer: when each layer is complex enough to review separately
- Plan then execute: when you're unsure about the scope
Worked example — scaffolding a module with "types first":
Say you need a complete invoicing module. Start with just the types:
> Create src/types/invoice.ts with these interfaces:
> - Invoice (id, customerId, items, total, status, issuedAt, dueAt)
> - InvoiceItem (id, description, quantity, unitPrice, lineTotal)
> - InvoiceStatus (draft | sent | paid | overdue | cancelled)
>
> Don't implement anything else yet.
Claude creates one clean file. You review it:
> Looks good, but add a "currency" field to Invoice (ISO 4217 code)
> and a "taxRate" field to InvoiceItem.
Now your types are locked. Every subsequent step references these types, and you know the data model is correct before writing a line of business logic. This prevents the painful situation where you build 6 files and then realise the types are wrong — requiring changes everywhere.
Questions & Answers
Key Takeaways
- Claude navigates automatically — describe intent, let it find the files
- Multi-layer features: describe the full scope, let Claude plan the changes
- Refactoring shines — renames, extractions, and pattern changes across files
- "Follow existing patterns" — point to a reference module for consistency
- Manage complexity: types first → API → UI, or plan then execute
- Keep batches reviewable — under 10 files per change ideally
Next Steps: In Lesson 5 — Debugging with Claude, you'll learn how to describe bugs effectively and use Claude's analysis to find and fix issues fast.