800 lines. That's the hard limit on every route file in my codebase. Not a suggestion. A rule. Enforced by the project context file that every AI coding session reads before touching a single line.
This rule didn't come from a software architecture textbook. It came from watching Claude lose its goddamn mind at line 900.
I build with AI coding tools. Claude, GPT, Copilot. They're force multipliers. A senior dev with AI tools ships at 6-10x velocity. I've documented this across months of daily recaps with commit hashes and dollar figures. But here's the thing... AI tools have a context window. They can hold roughly 800 to 1,200 lines of code in working memory before they start hallucinating. Referencing functions that don't exist. Forgetting the import at line 12 by the time they're editing line 1,100. Generating code that contradicts what's already in the file.
This is not theoretical. I watched it happen in real time.
January 13, 2026. portfolio.tsx was 1,896 lines. One route file. Loader, action handler, 20+ action intents, state orchestration, and all the UI rendering. Every time I asked Claude to modify something, it would lose track of the component hierarchy. Edit the wrong function. Introduce bugs in code it couldn't see anymore.
The file was too big for the tool. The tool is non-negotiable. So the file had to change.
That January 13 session... I don't know, something clicked. 7 commits before noon. 12 by end of day. $9,300 in created value. That was during the same vacation velocity experiment where week 3 hit $11,625/day. I was basically speedrunning a refactor.
The centerpiece was the portfolio decomposition.
Before: portfolio.tsx at 1,896 lines. One file doing everything.
After:
| File | Lines | Job |
|---|---|---|
portfolio.tsx |
782 | Controller: loader, action, state |
finance-utils.ts |
95 | Shared formatters and constants |
types.ts |
160 | Shared interfaces |
PortfolioHeader.tsx |
65 | Net worth display |
AccountTypeSection.tsx |
108 | Account group with cards |
ManualAccountCard.tsx |
95 | Non-crypto account |
CryptoAccountCard.tsx |
165 | Crypto wallet with holdings |
AddAccountModal.tsx |
155 | New account form |
EditAccountModal.tsx |
120 | Edit account form |
UpdateBalanceModal.tsx |
85 | Quick balance update |
ManageHoldingsModal.tsx |
165 | Crypto holdings CRUD |
DeleteConfirmModal.tsx |
50 | Confirmation dialog |
Route dropped from 1,896 to 782 lines. Every extracted component under 170 lines. Each file does one job. Each file fits entirely in the AI's context window.
That refactor created a rule I wrote into the project context file that same day: route files have an 800-line limit.
EGO alert... I'm about to sound like I invented MVC. I didn't. The pattern that emerged is Route-as-Controller, Component-as-View. This has existed for 50 years. But the motivation is new.
Routes handle data. loader functions fetch. action functions mutate. The route component orchestrates state and decides what renders. That's it. No UI logic in the route. No 400-line JSX blocks. The route is a controller.
Components handle views. They receive props. They render UI. They don't fetch data. They don't know about loaders. Pure display logic with callbacks for user actions. Portable. Testable. Replaceable.
I didn't split these files because of architectural purity. I split them because my AI coding partner couldn't hold 1,896 lines in its head. The pragmatic reason and the "correct" reason turned out to be the same thing... which is either reassuring or deeply annoying depending on how you feel about accidental good engineering.
Either way... the pattern kept proving itself. Every time a file got fat, the AI got dumb. Every split made the AI sharp again.
| File | Before | After | Date |
|---|---|---|---|
portfolio.tsx |
1,896 | 782 | Jan 13 |
nexus.actions.ts |
1,427 | 125 (dispatcher) | Feb 1 |
god-mode.tsx |
838 | 348 | Feb 1 |
TemplateManagerSheet.tsx |
694 | 362 | Feb 1 |
IncomeSection.tsx |
643 | 173 | Feb 1 |
Dashboard |
473 | 183 | Feb 1 |
The nexus.actions.ts decomposition tells the story best. This was the same codebase that later went from one JSONB column to nine normalized tables -- the file split made that migration possible. 1,427 lines of action handlers. Twenty-plus CRUD operations for accounts, holdings, categories, bills, income, goals, snapshots. One massive switch statement. Split into 7 focused action files under an actions/ directory. Account actions. Budget actions. Goal actions. Income actions. Snapshot actions. Shared types. Barrel re-export that preserves existing import paths. The original file became a 125-line dispatcher that routes intents to the right handler.
Every individual file under 300 lines now. The AI can read the entire handler it's modifying. It doesn't confuse the delete_account handler with the delete_bill handler because they're in separate files. That's... kind of embarrassing that it matters? But it matters.
"But now you have more files."
Yeah. 80+ components across the platform. 7 action files where there was 1. A components/finance/nexus/ directory with 20 files. I hear it.
But each of those files gives you something. Understandable in isolation -- open BillRow.tsx, it's 80 lines, you know exactly what it does without reading anything else. The AI knows too. Testable in isolation -- the finance-calculator.server.ts has 66 tests, the sovereignty calculator has 51. Small files with clear inputs and outputs make testing natural, not heroic. Replaceable in isolation -- when the old portfolio components became dead code after the Nexus unified view shipped, I deleted 10 files in one commit. No surgical extraction from a monolith. Just rm and clean up the barrel export. AI-compatible -- every file fits in the context window. Every modification is scoped. The AI doesn't drift, doesn't hallucinate, doesn't forget what's at the top of the file while editing the bottom.
But yeah... here's what I didn't predict. The AI constraint produced better engineering.
Smaller files forced separation of concerns. Not because I was thinking about SOLID principles. Not because I was min/maxing some architecture score. Because the file was too big and something had to leave. What left was always the view logic, the utility functions, the type definitions. The stuff that shouldn't have been in the route file anyway.
The 800-line limit is an AI constraint that accidentally enforces good architecture. Every refactor I did for AI ergonomics also improved testability, readability, and maintainability. Not sometimes. Every time. I don't know if that says something profound about software engineering or if I just got lucky... but I'll take it.
Regardless... if you're building with AI tools and your files are over 800 lines, you're fighting your own toolchain. The AI will lose context. It will make mistakes. You'll spend more time fixing AI-generated bugs than you saved by using AI in the first place.
800 lines. Route handles data. Component handles UI. Each file does one job.
This isn't architecture advice. It's logistics.