← All posts

How to Structure Your Next.js App for Scale

Starting simple doesn't mean ending messy. Here's how I structure Next.js apps to handle growth without refactoring every 6 months.

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()), };

What Scales and What Doesn’t

Approach Scales? All components in /components ❌ Feature-based structure ✅ Server Actions for mutations ✅ API routes for everything ❌ Shared hooks/lib ✅
← All posts