DEV Community

Alex Rogov
Alex Rogov

Posted on • Originally published at alexrogov.hashnode.dev

How to Structure a TypeScript Project So AI Agents Can Navigate It

Your AI coding assistant is only as good as the codebase it navigates. I've watched Claude Code, Cursor, and Copilot struggle with the same project structures that trip up junior developers — and excel in codebases designed with clear boundaries.

After restructuring 8 TypeScript projects specifically to work better with AI agents, here's what actually moves the needle.

Why Project Structure Matters More Now

When you ask an AI agent to "add a new endpoint for user notifications," it needs to:

  1. Find where endpoints live
  2. Understand the existing patterns
  3. Locate related code (models, services, types)
  4. Follow the conventions already established

In a well-structured project, the agent finds all of this in seconds. In a messy one, it hallucinates paths, invents patterns that don't match your codebase, and produces code you'll spend 20 minutes fixing.

The difference isn't the AI model — it's the signal-to-noise ratio in your file tree.

The Structure That Works

Here's the folder structure I use across all my TypeScript projects (NestJS, Next.js, Express):

src/
├── domain/                    # Pure business logic, zero dependencies
│   ├── user/
│   │   ├── user.entity.ts
│   │   ├── user.value-objects.ts
│   │   └── user.errors.ts
│   └── order/
│       ├── order.entity.ts
│       └── order.errors.ts
├── application/               # Use cases + port interfaces
│   ├── user/
│   │   ├── use-cases/
│   │   │   ├── create-user.ts
│   │   │   └── update-user.ts
│   │   └── ports/
│   │       ├── user.repository.ts
│   │       └── email.port.ts
│   └── order/
│       ├── use-cases/
│       └── ports/
├── infrastructure/            # Framework + external implementations
│   ├── database/
│   │   ├── prisma-user.repository.ts
│   │   └── prisma-order.repository.ts
│   ├── http/
│   │   ├── controllers/
│   │   │   ├── user.controller.ts
│   │   │   └── order.controller.ts
│   │   ├── middleware/
│   │   └── dto/
│   │       ├── create-user.dto.ts
│   │       └── update-user.dto.ts
│   └── external/
│       ├── email.service.ts
│       └── payment.gateway.ts
├── shared/                    # Cross-cutting utilities
│   ├── types/
│   ├── utils/
│   └── constants/
└── CLAUDE.md                  # AI agent instructions
Enter fullscreen mode Exit fullscreen mode

Why this works for AI agents: every folder name is a clear signal. When the agent sees application/user/use-cases/, it knows exactly what goes there — and more importantly, what doesn't.

Rule 1: One Concept Per File

This is the single biggest improvement you can make for AI navigation.

// ❌ BAD: user.types.ts — 200 lines of mixed concerns
export interface User { ... }
export interface UserProfile { ... }
export interface CreateUserDto { ... }
export interface UpdateUserDto { ... }
export type UserRole = 'admin' | 'editor' | 'viewer';
export type UserPermission = 'read' | 'write' | 'delete';
export interface UserFilters { ... }
export interface PaginatedUsers { ... }
Enter fullscreen mode Exit fullscreen mode
// ✅ GOOD: split by concept
// domain/user/user.entity.ts
export interface User {
  id: string;
  email: string;
  role: UserRole;
  profile: UserProfile | null;
  createdAt: Date;
}

export type UserRole = 'admin' | 'editor' | 'viewer';

// domain/user/user-profile.entity.ts
export interface UserProfile {
  displayName: string;
  avatarUrl: string | null;
  bio: string;
}

// infrastructure/http/dto/create-user.dto.ts
export class CreateUserDto {
  @IsEmail()
  email: string;

  @MinLength(8)
  password: string;

  @IsOptional()
  displayName?: string;
}
Enter fullscreen mode Exit fullscreen mode

When an AI agent searches for "User entity," it finds user.entity.ts — not a 200-line grab bag where it has to parse which interface is the domain entity vs. the DTO vs. the filter type.

Rule 2: Predictable Naming Conventions

AI agents learn patterns from your existing files. If your naming is consistent, the agent extrapolates correctly. If it's inconsistent, every new file is a coin flip.

✅ Consistent pattern:
  create-user.ts        → CreateUser class
  update-user.ts        → UpdateUser class
  delete-user.ts        → DeleteUser class
  create-order.ts       → CreateOrder class

❌ Inconsistent naming:
  createUser.ts         → CreateUserUseCase class
  update_user.ts        → UpdateUser class
  deleteUserHandler.ts  → UserDeleteHandler class
  newOrder.ts           → OrderCreation class
Enter fullscreen mode Exit fullscreen mode

My naming rules (defined in CLAUDE.md):

  • Files: kebab-case, suffix indicates type — .entity.ts, .repository.ts, .controller.ts, .dto.ts, .port.ts
  • Classes: PascalCase, no suffix redundancy — CreateUser not CreateUserUseCase
  • Interfaces: PascalCase, prefix with purpose — UserRepository, EmailPort
  • Test files: same name + .spec.tscreate-user.spec.ts

Rule 3: Explicit Dependency Direction

This is where most projects fall apart for AI agents. When imports go in every direction, the agent can't predict where to add new code.

// ❌ Infrastructure importing from other infrastructure
// infrastructure/database/prisma-user.repository.ts
import { EmailService } from '../external/email.service';  // Wrong layer!
import { UserController } from '../http/controllers/user.controller';  // Circular!

// ✅ Clean dependency direction: domain ← application ← infrastructure
// infrastructure/database/prisma-user.repository.ts
import { User } from '../../domain/user/user.entity';
import { UserRepository } from '../../application/user/ports/user.repository';
Enter fullscreen mode Exit fullscreen mode

The rule is simple: imports only point inward. Infrastructure → Application → Domain. Never the reverse.

I enforce this with a simple TypeScript path alias in tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@domain/*": ["src/domain/*"],
      "@application/*": ["src/application/*"],
      "@infrastructure/*": ["src/infrastructure/*"],
      "@shared/*": ["src/shared/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When the AI sees @domain/user/user.entity, it immediately knows the layer. No ambiguous relative paths like ../../../models/user.

Rule 4: CLAUDE.md at the Root

Every project gets a CLAUDE.md that tells the agent how to navigate:

## Project Structure
- `src/domain/` — pure entities, value objects, domain errors. ZERO external imports.
- `src/application/` — use cases and port interfaces. Only imports from domain.
- `src/infrastructure/` — framework code, DB, HTTP, external services.
- `src/shared/` — cross-cutting utilities used by all layers.

## Conventions
- One class/interface per file
- File names: kebab-case with type suffix (.entity.ts, .repository.ts)
- Use cases: one per file in `application/{module}/use-cases/`
- New endpoint = controller method + DTO + use case + port (if needed)

## Adding a New Feature
1. Define entity in `domain/{module}/`
2. Create use case in `application/{module}/use-cases/`
3. Define ports in `application/{module}/ports/`
4. Implement infrastructure in `infrastructure/`
5. Wire up in module file
6. Run `npm run typecheck && npm run test`
Enter fullscreen mode Exit fullscreen mode

This isn't just documentation — it's a navigation map. The AI reads this first and knows exactly where to put new code, what patterns to follow, and what commands to run for verification.

Rule 5: Kill Barrel Exports

This one surprised me. Barrel exports (index.ts files that re-export everything) actually hurt AI agent performance:

// ❌ src/domain/user/index.ts
export * from './user.entity';
export * from './user-profile.entity';
export * from './user.errors';
export * from './user.value-objects';
Enter fullscreen mode Exit fullscreen mode

The problem: when the AI encounters import { User } from '@domain/user', it doesn't know which file contains User. It has to open the barrel, scan all re-exports, then find the source file. With large barrels (20+ exports), the agent frequently picks the wrong source when it needs to modify the definition.

// ✅ Direct imports — AI knows exactly where to look
import { User } from '@domain/user/user.entity';
import { UserNotFoundError } from '@domain/user/user.errors';
Enter fullscreen mode Exit fullscreen mode

Direct imports are longer, but they're unambiguous. The AI can jump straight to the right file. The trade-off is worth it.

Rule 6: Co-locate Tests

❌ Separate test directory:
src/
  application/user/use-cases/create-user.ts
tests/
  unit/
    application/
      user/
        use-cases/
          create-user.spec.ts    ← 5 directories deep, mirrors src

✅ Co-located tests:
src/
  application/user/use-cases/
    create-user.ts
    create-user.spec.ts          ← right next to the source
Enter fullscreen mode Exit fullscreen mode

When the AI modifies create-user.ts, it naturally finds create-user.spec.ts in the same directory. No searching through a mirrored test tree. It updates both files in one pass.

The Proof: Before vs After

I restructured a 40K-line NestJS project using these rules. Here's what changed in my AI-assisted workflow:

Metric Before (flat structure) After (layered + conventions)
AI finds correct file on first try ~60% ~95%
Generated code follows project patterns ~40% ~85%
"Fix the imports" follow-up prompts 3-4 per feature 0-1 per feature
Time from prompt to working code 8-12 min 2-4 min

The biggest win wasn't any single rule — it was the combination. When naming is predictable AND dependencies flow one direction AND CLAUDE.md explains the patterns, the AI connects the dots.

What This Doesn't Solve

To be clear: structure alone doesn't make AI write correct business logic. It still:

  • Misses edge cases in your domain rules
  • Oversimplifies error handling (as I covered in my previous article)
  • Doesn't understand your specific performance requirements
  • Can't infer undocumented business constraints

Structure makes the AI a better navigator. Your job is still being the architect.

Key Takeaways

  • One concept per file — the single biggest improvement for AI navigation
  • Predictable naming — consistent conventions let AI extrapolate patterns correctly
  • Explicit dependency direction — imports only point inward (infrastructure → application → domain)
  • CLAUDE.md at the root — a navigation map the AI reads first before touching any code
  • Kill barrel exports — direct imports give AI unambiguous file locations
  • Co-locate tests — the AI updates source and tests in one pass

Your codebase is the AI's context window. Make it scannable, predictable, and unambiguous — and the AI goes from "occasionally useful" to "consistently reliable."


More practical guides on AI-augmented architecture on Twitter/X. Connect on LinkedIn for the discussion.


Originally published on my Hashnode blog.

Top comments (1)

Collapse
 
ali_muwwakkil_a776a21aa9c profile image
Ali Muwwakkil

One insight from our accelerator is that AI agents often struggle not with the complexity of TypeScript, but with inconsistent codebase structure. A well-organized project with clear naming conventions and modular architecture significantly enhances an AI agent's ability to navigate and assist. It's less about the language and more about predictability and clarity in your codebase. - Ali Muwwakkil (ali-muwwakkil on LinkedIn)