Why boundary mistakes get through review so easily
AI-generated App Router code often looks plausible because both server and client components can render the same JSX shape. The mistake is usually not visual. It sits in where data is fetched, where browser APIs are read, and where stateful interactivity begins.
That creates a specific review problem. A diff can look tidy while still pushing fetch logic into the client, leaking browser-only code into server paths, or marking whole trees with use client because that was the easiest way for the model to make the code compile.
The checklist that catches most App Router boundary bugs
Keep the review tight. You are checking whether the code chose the right side of the boundary, not whether the file structure feels elegant.
- Data source: is data fetched on the server by default, or was it moved client-side only to make hooks easier to generate?
- Browser dependency: does this component actually need browser APIs, local state, or event handlers before adding use client?
- Serialization: are props crossing the boundary serializable, or is the code trying to pass functions, class instances, or live connections?
- Tree size: did one interactive child cause the whole parent subtree to become client-rendered?
- Mutation path: if the UI mutates data, is the server or action boundary still explicit instead of hidden inside a client fetch patch?
- Caching intent: does the chosen boundary preserve the caching and streaming behavior the page should have?
Questions worth asking in pull requests
Good review comments force the author to explain tradeoffs. Ask why this file is client-rendered, what would break if the parent stayed server-side, and which prop must cross the boundary.
Another useful question is whether the interactive island can be pushed lower in the tree. That one question often removes a large accidental client surface created by AI-generated scaffolding.
A safe migration order when the boundary is already blurry
Start by moving pure data fetches and static layout back to server components. Then isolate the smallest interactive leaf that truly needs hooks, state, or DOM access. Only after that should you refine mutation and caching behavior.
If the code still feels confusing after that split, the draft is probably carrying too much generated structure. Delete the wrapper components and rebuild the tree around the actual boundary instead of preserving accidental complexity.