A few months ago I realized that as the code and modules inside the structure we’d built kept growing, it was starting to become a problem, and developing with AI was getting really hard.
I’d ask Cursor or Claude Code to make a very simple change to a frontend module. At first glance it looked great. Then I’d open the PR and inspect it: it had done state management differently from the rest of the app, it had written copies of helper functions that already existed, it had pulled some random third-party package we never use into the project, and it had completely ignored the configs we’d added for design.
At first I thought it was the LLM’s fault, because everyone around me was complaining about this too — model A is terrible, model B is too expensive, that kind of thing. I looked for the problem in the small context or in the model hallucinating, but the problem was entirely in our architecture.
If I had written the code myself, I would have known easily what goes where, since I set up the architecture, and I could apply everything in line with it. But if I don’t explain these things to the AI, how is it supposed to know, right? If it inspected all the code, the context would bloat, and this time we might get worse results than the ones we got without following the architecture at all. If our codebase is complex, the agent’s context bloats and its reasoning breaks down fast.
If we want AI agents to write clean, production-ready code, just writing better prompts or waiting for the next model update isn’t enough. We need to build agent-native frontend architectures.
Here are the ways to make our architecture ideal for AI agents.
1. Monorepo as a Context Management Strategy
Monorepos used to be used for trunk-based development — to shorten development processes when multiple teams were developing a single platform, and so on. But now thousands of agents are developing for a single platform. Where it used to make three teams’ work easier, now thousands of agents’ work gets easier.
If you’ve split your micro frontends into separate repos, then to use a util from repo A — or something similar to it — in repo B, you have to download both repos and include them in the context. In that case, even for a very simple task, you lose time, you spend tokens, and sometimes you can’t even reach a result.
In a monorepo (we use Turborepo with pnpm workspaces), everything lives under one roof, but with explicit boundaries:
repo/
├── apps/
│ ├── shell/ # Host application (thin orchestration only)
│ ├── module-orders/ # Independent micro frontend
│ └── module-catalog/ # Independent micro frontend
├── packages/
│ ├── ui/ # Shared component library (shadcn/tailored)
│ ├── utils/ # Pure helper functions (formatters, calculations)
│ ├── analytics/ # Single source of truth for tracking schema
│ └── config/ # Base tsconfig, eslint, tailwind presets
├── turbo.json
└── package.json
When I ask an agent to work on module-orders, it doesn’t scan the whole repository. It reaches only the shared packages/ folder it actually uses, directly through local workspace references (workspace:*).
On a single repository, the agent can move around freely and reach what it’s looking for easily.
2. Independent Micro Frontends: Enforcing Strict Boundaries
This is actually the hardest part of a monorepo — that the boundaries aren’t drawn with strict rules. If we don’t separate the modules and packages from each other properly, development can happen in places we don’t want during the process, and that development can make it all the way to prod, and as you know, the result is an incident.
The solution to this problem is completely independent micro frontends and packages.
The core rule is strict: modules don’t connect horizontally to each other. An agent working on module-orders should never need to know anything about module-catalog or import from it. They connect only vertically, through shared packages and the shell application.
┌─────────────────────────────┐
│ shell │ ← Orchestration & routing only
└───────┬────────────┬────────┘
│ │
┌─────────▼────┐ ┌───▼─────────┐
│ orders │ │ catalog │ ← Completely isolated modules
└─────┬────────┘ └──────┬──────┘
│ │
└────────┬──────────┘
▼
┌────────────┐
│ packages/ │ ← Shared, versioned foundation
└────────────┘
The Version Decoupling Trick
To prevent a change in a shared helper from breaking all modules at once, we decoupled our internal packages from each other.
When packages/utils ships v2.1.0 with a new formatCurrency helper:
module-orders moves to “utils”: “2.1.0” right away.
module-catalog stays on “utils”: “2.0.0” until we explicitly decide to update it.
As for why this matters for AI: when I tell Cursor or Claude “work only inside apps/module-orders,” the boundaries are physically real. The agent can’t break the catalog module, because there’s no horizontal import path connecting them.
This way, we ended up using isolation as a security mechanism at the same time.
3. Tiered AGENTS.md: Preventing Convention Drift
If you don’t give an AI agent explicit rules, it falls back on the average of its training data, which usually means generic, Stack Overflow-style code from 2024. It writes raw fetch calls instead of your custom API wrapper, or inline Tailwind classes instead of your design configs — maybe it even writes CSS.
As a solution, AGENTS.md (or .cursorrules) is used. But there’s a point to watch out for: not every rule should be always-allow. Only the ones you want to run every single time should be.
If you make all the rules always-allow, you’ve bloated the context before you even give your own prompt, and as a result you’ve dropped your efficiency quite a bit. Research shows that after roughly 3,000 tokens of context load, reasoning quality drops noticeably.
We solved this with tiered rule management:
.rules/
├── always/
│ └── global-conventions.md # Universal rules (naming, tech stack, base TS config)
├── modules/
│ ├── orders.md # Specific ONLY to the orders domain
│ └── catalog.md # Specific ONLY to catalog behavior
└── packages/
├── analytics.md # Event tracking contracts
└── utils.md # Helper function usage rules
An example of a strict package rule (analytics.md):
markdown
# packages/analytics usage rule
- NEVER call `window.dataLayer.push` directly.
- ALWAYS use the exported `trackEvent()` utility from `packages/analytics`.
- Event names MUST be defined in `packages/analytics/events.ts`.
- Required fields for every event: `event_name` (snake_case), `category`, `module`.
When an agent edits an analytics file, it loads analytics.md. When it edits a CSS theme, it ignores the analytics rules completely.
General rule: write the rule the moment you establish a new pattern, not six months later. If you forget, you won’t remember it again — proven by experience 🙂 Pair this with strict linters so the agent gets immediate feedback when it violates a rule.
How I Prompt Agents Now
When your architecture is clean, writing prompts gets much easier. You no longer need to write long paragraphs. Just defining the scope, the target module, and the rule path is enough.
Adding a feature to an isolated module:
Work only inside apps/module-catalog. Don’t touch any other app directory.
Task: add a ‘Bookmark’ button to the product card.
- Use the existing Button component from packages/ui.
- Emit the event using trackEvent from packages/analytics.
- Follow the rules in .rules/packages/analytics.md.
Introducing a shared pattern (along with its rule):
Add a formatCurrency helper inside packages/utils.
- Write the function in packages/utils/currency.ts and export it.
- At the same time, update .rules/packages/utils.md with: ‘Always use formatCurrency for price displays; no manual string concatenation.’
- Bump the package version. Don’t update the other apps yet.
Conclusion
Most of the time we spend our focus on improving our prompts or waiting for a “smarter” LLM model to come out. But most of the time the bottleneck isn’t the model — it’s a messy repo. If you want to get maximum efficiency from AI tools, you have to give them a well-designed environment:
- A monorepo for direct context access.
- Micro frontends for strict, unbreakable boundaries.
- Tiered AGENTS.md to enforce project conventions without choking the context window.
- Clean boundaries for your code mean clean context for your agent.
What does your current project structure look like? Are your AI tools succeeding in it, or drowning in context bloat? Share below.
Every “Frontend Wars” piece here eventually collapses into the same question: which stack do you actually pick for this project? I’m building Stack It Fast to answer that with data instead of vibes. Answer a few questions about your project, get a stack recommendation backed by what similar projects actually shipped with, not what’s trending on Twitter this week.