Why Traditional Frameworks Hide Too Much for AI Agents
Every backend framework makes the same bet: hide complexity behind conventions, and developers will be productive. Rails chose convention over configuration. NestJS chose decorators and dependency injection. Express chose middleware chains. These bets paid off for human developers who learn patterns over months and carry the system's architecture in their heads.
They do not pay off when the consumer of that architecture is an AI agent.
Convention Over Configuration Assumes a Human Learner
When Rails places a controller in app/controllers/users_controller.rb, a human developer understands
that it handles routes for the User resource. The naming convention encodes the relationship between
the file, the model, and the routes. No configuration file spells this out. The developer just knows.
An AI agent does not "just know." It can be told, and it can pattern-match, but conventions are not formal contracts.
They are social agreements between humans. When the agent encounters app/controllers/billing_portal_controller.rb,
it has to guess whether that maps to a BillingPortal model, a billing_portals table,
or something else entirely. Sometimes it guesses right. Sometimes it produces code that follows the naming convention
but violates the actual architecture.
The problem is not that conventions are bad. They are excellent compression mechanisms for human teams. The problem is that they encode architectural intent in a format that only works for agents who have been socialized into the conventions over time. AI agents are not socialized. They are instantiated.
Decorator Semantics Are Framework-Specific
Consider a NestJS controller:
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
@Post()
@Roles('admin')
@UseInterceptors(AuditLogInterceptor)
async createUser(@Body() dto: CreateUserDto) {
// ...
}
}
To a human who has worked with NestJS, this is readable. @UseGuards(AuthGuard) means authentication
is required. @Roles('admin') means only admins can access this endpoint. @UseInterceptors(AuditLogInterceptor)
means the action is logged.
To an AI agent, these are function calls with opaque semantics. The agent can see that decorators are applied,
but it cannot reliably determine what AuthGuard actually does without reading its implementation,
tracing its dependencies, and understanding the NestJS execution pipeline. If the agent needs to add a new endpoint,
it might forget @UseGuards, apply decorators in the wrong order, or use the wrong guard class.
These are not compilation errors. They are architectural violations that pass type-checking and fail at runtime
or, worse, fail silently.
The decorator pattern conflates declaration with implementation. You declare that a guard is applied, but the behavior of that guard lives in a separate file with its own dependencies and side effects. There is no single artifact that says: "this endpoint requires authentication, restricts to admin roles, and logs all mutations." That information is assembled at runtime by the framework's decorator resolution pipeline.
Middleware Ordering Is Implicit
Express middleware runs in registration order. If you register the rate limiter before the authentication middleware,
unauthenticated requests consume rate limit quota. If you register the body parser after the route handler,
req.body is undefined. These ordering dependencies are not declared anywhere. They exist in the
sequence of app.use() calls in a file that the developer wrote and, ideally, documented.
AI agents generating Express code routinely produce middleware in the wrong order. This is not because the agents are unintelligent. It is because the correct order is not specified by any formal artifact. It is specified by the developer's mental model of request flow, which is not present in the codebase.
You can work around this by adding comments. You can add linter rules. You can write tests that verify middleware order. But these are patches on a fundamental problem: the framework does not treat middleware ordering as a first-class architectural concern. It treats it as an implementation detail.
Module Boundaries Exist Only in Developer Heads
In most frameworks, modules are organizational suggestions. A billing/ directory and an auth/
directory imply a boundary, but nothing enforces it. A service in billing/ can import from auth/,
auth/ can import from billing/, and circular dependencies emerge gradually without any
tool raising an alarm.
When an AI agent is asked to add a feature, it has no way to determine which module should own the new code, which imports are allowed, and which would create an architectural violation. It can look at existing imports for patterns, but existing code may already contain violations that accumulated over months.
Without explicit module boundary declarations, the agent is navigating by dead reckoning. It might produce code that works but that a senior engineer would reject in review because it crosses a boundary that is obvious to the team but invisible in the codebase.
The Cost
The cost of hidden architecture is not that AI agents cannot generate code. They can. Modern language models produce syntactically correct, type-safe code with impressive reliability. The cost is that the generated code compiles but violates architecture.
A team using AI agents with a traditional framework ends up in a constant review cycle: the agent writes code, a human reviews it for architectural violations, the human explains the violation, the agent tries again. This cycle persists because the architectural knowledge lives in the human's head, not in the system.
The fix is not smarter AI agents. It is more explicit architecture. If the system's structure, constraints, boundaries, and rules are formal artifacts that agents can read and traverse, then the agents can respect those structures before writing a single line of code. Not because they are intelligent enough to infer the architecture, but because the architecture is declared in a format they can consume.
This is not a theoretical problem. Any team that has used Copilot, Cursor, or an AI coding agent on a non-trivial codebase has encountered it. The code looks right. It passes the type checker. And it violates an invariant, crosses a module boundary, or introduces a side effect that the framework's conventions were supposed to prevent.
The frameworks we have were designed for human developers who carry context. The frameworks we need are designed for a world where both humans and AI agents operate on the same codebase, and the architecture must be readable by both.