All articles
Architecture

AI-Optimized Code Architecture

How to structure your React and TypeScript codebase so AI assistants like Claude Code, Copilot, and Cursor can generate accurate, idiomatic code on the first try.

Share:
Sam Rivera
Sam Rivera
April 20, 20269 min read

Every developer using AI coding assistants has experienced the frustration: the AI writes code that almost works, but uses the wrong naming conventions, imports from the wrong path, or invents an API that doesn't exist. The root cause is almost always poor codebase structure, not a limitation of the AI.

AI assistants are context-prediction engines. They generate the most statistically likely continuation given the context window. A well-structured codebase creates strong, consistent signals that dramatically improve generation quality.

The AI Code Generation Stack

Here's how an AI assistant processes a code generation request:

graph LR
    A["User Prompt\n'Add a user settings form'"] --> B["Context Gathering\n• Open files\n• Recent edits\n• File tree"]
    B --> C["Pattern Matching\n• Naming conventions\n• Import patterns\n• API contracts"]
    C --> D["Code Generation\n• Follows existing patterns\n• Uses real APIs\n• Matches style"]
    D --> E["Output\nIdiomatic, correct code"]

    style A fill:#dbeafe
    style E fill:#dcfce7

The middle steps — context gathering and pattern matching — are where your codebase structure matters most.

Naming Consistency is Everything

The #1 improvement you can make: ruthless naming consistency. AI learns your patterns from examples in context.

ts
// Bad — inconsistent patterns confuse AI
const getUser = () => {...}
const fetchUserProfile = () => {...}
const loadUserSettings = () => {...}
const retrieveUserPosts = () => {...}

// Good — consistent verb-noun pattern
const fetchUser = () => {...}
const fetchUserProfile = () => {...}
const fetchUserSettings = () => {...}
const fetchUserPosts = () => {...}

When the AI sees fetchUser and fetchUserProfile, it correctly infers fetchUserSettings on the first try.

Colocate by Feature, Not by Type

Most AI errors come from the AI not knowing which component/hook/utility to use. Feature-based colocation keeps related code together so the AI has full context:

src/features/
  auth/
    components/
      LoginForm.tsx
      RegisterForm.tsx
    hooks/
      useAuth.ts
      useAuthRedirect.ts
    api/
      authApi.ts
    types/
      auth.types.ts
    index.ts          ← barrel export
  posts/
    components/...
    hooks/...
    api/
      postsApi.ts
    types/
      post.types.ts
    index.ts

When you open LoginForm.tsx, the AI can see useAuth.ts is nearby and will import from it rather than inventing its own auth logic.

Explicit Type Contracts

Define types in one place and import them everywhere. AI tools respect explicit type definitions far better than inferred types spread across files:

ts
// features/posts/types/post.types.ts
export interface Post {
  id: string
  title: string
  content: string
  authorId: string
  status: 'draft' | 'published' | 'archived'
  createdAt: Date
  updatedAt: Date
}

export interface CreatePostInput {
  title: string
  content: string
  status?: Post['status']
}

export interface PostFilters {
  status?: Post['status']
  authorId?: string
  page?: number
  limit?: number
}

When the AI generates a new hook or component that works with posts, it imports these types correctly because they're named and exported clearly.

Barrel Exports Signal Boundaries

index.ts files act as public API declarations for your modules. AI tools respect these boundaries:

ts
// features/posts/index.ts
export { PostList } from './components/PostList'
export { PostDetail } from './components/PostDetail'
export { usePost, usePosts } from './hooks/usePosts'
export type { Post, CreatePostInput } from './types/post.types'
// Don't export internal implementation details

The AI will import from features/posts rather than reaching into internal paths.

Consistent Error Handling Patterns

Define your error handling once, use it everywhere:

ts
// lib/errors.ts
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message)
    this.name = 'AppError'
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} with id "${id}" not found`, 'NOT_FOUND', 404)
  }
}

export class ValidationError extends AppError {
  constructor(message: string, public field?: string) {
    super(message, 'VALIDATION_ERROR', 400)
  }
}

Once the AI sees this pattern in your codebase, it generates correct error handling for all new features without prompting.

Document Your Invariants

Brief comments on non-obvious architectural decisions dramatically improve AI output quality:

ts
// hooks/useOptimisticUpdate.ts

// INVARIANT: Always call onSettled regardless of success/failure.
// This ensures we re-fetch from server and don't get stuck with
// stale optimistic state. Never return early from onMutate.
export function useOptimisticUpdate<T>(...) {
  return useMutation({
    onMutate: async (variables) => {
      // ...
    },
    onSettled: () => {
      queryClient.invalidateQueries(...)  // Always runs
    },
  })
}

Putting It Together

A well-architected codebase for AI assistance has:

  1. Consistent naming — AI predicts names correctly from patterns
  2. Feature colocation — related code is in context together
  3. Explicit type contracts — AI imports real types, not invented ones
  4. Barrel exports — clear module boundaries the AI respects
  5. Documented invariants — AI learns your architectural rules

The templates in this repo follow all these principles. That's why developers report generating complete features with a single prompt — the codebase gives the AI everything it needs to succeed.

Back to all articles
Previous

Type-Safe Routing: A Deep Dive

Explore how TanStack Router achieves end-to-end type safety for routes, params, search params, and loader data — and why it matters for large teams.

Next

From Next.js to TanStack Start: A Migration Guide

A practical, honest look at migrating a production Next.js App Router application to TanStack Start — what's better, what's harder, and what to expect.