A bad file structure slows down every feature. A good one fades into the background.
The Structure I Use
app/
├── (routes)/ # Route groups
│ ├── (auth)/ # Auth routes
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/ # Protected routes
│ └── api/ # API routes
├── components/ # Shared UI components
│ ├── ui/ # Base components
│ └── features/ # Feature-specific
├── lib/ # Utilities
├── hooks/ # Custom hooks
├── types/ # TypeScript types
└── services/ # External integrations
Key Principles
1. Co-locate Related Code
Keep what changes together, together:
``` // ❌ Bad: Scattered components/Button.tsx hooks/useAuth.ts lib/auth.ts
// ✅ Good: Feature-based features/auth/ ├── components/ ├── hooks/ └── lib/ ```
2. Extract Shared Logic
When you use something 3+ times, extract it:
typescript
// lib/use-fetch.ts
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
// ... fetch logic
return { data, loading, error };
}
3. Feature Folders for Complex Logic
features/
├── auth/
│ ├── components/
│ │ ├── LoginForm.tsx
│ │ └── AuthProvider.tsx
│ ├── hooks/
│ │ └── useAuth.ts
│ ├── actions/
│ │ └── auth.ts (Server Actions)
│ └── types/
│ └── index.ts
The “Facade Pattern”
Expose clean APIs, hide implementation:
typescript
// features/products/api.ts
export const productsApi = {
list: () => fetch('/api/products').then(r => r.json()),
get: (id: string) => fetch(`/api/products/${id}`).then(r => r.json()),
create: (data: CreateProductDTO) =>
fetch('/api/products', {
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json()),
};