Back to blog
·5 min read

How to Structure a Scalable Next.js Project with App Router: Feature-First Architecture Guide

Most developers put everything in the app/ folder and hit a wall at 200+ files. Here's the exact structure that keeps Next.js projects maintainable at scale, plus how AI coding tools work better with organized code.

The Next.js Structure Problem That Hits Every Growing Team

You start a new Next.js project. The create-next-app defaults feel clean. Your app/ folder has a few routes, some components, a utils folder. Everything works.

Then you add authentication. Then a dashboard. Then a payment flow. Then analytics. Six months later, you have 200+ files scattered across components/, hooks/, utils/, lib/, and you spend more time searching for where something lives than actually building features.

This is the most common architecture mistake in Next.js projects today, and it gets worse as teams try to use AI coding assistants on poorly organized codebases. When your context is scattered, Claude Code wastes tokens. Your IDE struggles. Your developers waste time navigating chaos.

The solution isn't complicated, but it requires thinking differently about how you organize your code.

Why Type-Based Organization Fails at Scale

The default instinct is to organize by file type:

  • components/Button.tsx
  • components/Form.tsx
  • hooks/useAuth.ts
  • hooks/useFetch.ts
  • utils/helpers.ts
  • lib/db.ts
  • This works fine for a TODO app. But in production, when you're building a multi-tenant SaaS with dashboards, user settings, payment processing, and reporting, this structure becomes your enemy.

    Here's why: When you need to change how authentication works, you touch files in components/, hooks/, utils/, lib/, and middleware/. Everything is scattered. You can't see what's related at a glance. Adding new features means hunting through five different folders to understand the existing patterns.

    Worse, if you're using Claude Code or another AI assistant to help build features, this scattered structure forces you to give it huge amounts of context just to understand one feature's dependencies.

    The Feature-First Directory Structure That Works

    Instead of organizing by type, organize by feature domain:

    ```

    src/

    ├── app/

    │ ├── layout.tsx

    │ ├── page.tsx

    │ ├── (auth)/

    │ │ ├── login/page.tsx

    │ │ ├── signup/page.tsx

    │ │ └── layout.tsx

    │ ├── (dashboard)/

    │ │ ├── layout.tsx

    │ │ ├── page.tsx

    │ │ └── settings/page.tsx

    │ └── api/

    │ ├── auth/route.ts

    │ └── users/route.ts

    ├── features/

    │ ├── auth/

    │ │ ├── components/

    │ │ │ ├── LoginForm.tsx

    │ │ │ └── SignupForm.tsx

    │ │ ├── hooks/

    │ │ │ ├── useAuth.ts

    │ │ │ └── useSession.ts

    │ │ ├── utils/

    │ │ │ └── validators.ts

    │ │ ├── types/

    │ │ │ └── index.ts

    │ │ └── services/

    │ │ └── authService.ts

    │ ├── dashboard/

    │ │ ├── components/

    │ │ │ ├── DashboardHeader.tsx

    │ │ │ └── StatsCard.tsx

    │ │ ├── hooks/

    │ │ │ └── useDashboardData.ts

    │ │ └── types/

    │ │ └── index.ts

    │ └── payments/

    │ ├── components/

    │ ├── hooks/

    │ ├── types/

    │ └── services/

    ├── shared/

    │ ├── components/

    │ │ ├── Button.tsx

    │ │ └── Modal.tsx

    │ ├── hooks/

    │ │ └── useMediaQuery.ts

    │ ├── utils/

    │ │ └── cn.ts

    │ └── types/

    │ └── index.ts

    └── config/

    ├── env.ts

    └── constants.ts

    ```

    Here's what changed:

  • app/ folder is strictly for routing and layout. Nothing else lives here.
  • features/ folder contains self-contained domains (auth, dashboard, payments). Each feature owns its components, hooks, types, and utilities.
  • shared/ folder contains reusable primitives used across multiple features (buttons, modals, utility hooks).
  • config/ is for global configuration, environment setup, and constants.
  • Why This Structure Works Better

    When you build a feature in this structure:

  • Everything related lives together. If you're working on the auth feature, you navigate one folder. No hunting across the codebase.
  • Dependencies are obvious. If auth/ imports from payments/, that's a code smell. Your structure prevents accidentally coupling unrelated domains.
  • Scaling is natural. Add a new feature? Create a new folder in features/. No decisions about where things go.
  • Shared code stays minimal. The shared/ folder should be small. If it's growing, ask: "Is this really shared, or should it belong to a specific feature?"
  • AI coding tools work better. When you run Claude Code with /context on this structure, the assistant understands feature boundaries. It knows what's related. It can be more precise with changes.
  • Practical Implementation Tips

    Start with features that represent user-facing domains, not technical boundaries. Instead of creating a forms/ feature or a validation/ feature, create auth/, dashboard/, onboarding/. These map to actual user journeys.

    Use index.ts files in each feature to control what exports publicly:

    ```

    // features/auth/index.ts

    export { useAuth } from './hooks/useAuth';

    export { LoginForm } from './components/LoginForm';

    export type { User, AuthState } from './types';

    ```

    This creates a clean API for your feature. Other parts of the app import from features/auth, not features/auth/hooks/useAuth.

    Don't over-engineer too early. Start with features/ and shared/, add subdirectories as each grows. A small auth feature might just have:

    ```

    features/auth/

    ├── components.tsx

    ├── hooks.ts

    ├── types.ts

    └── service.ts

    ```

    Flatten it when it's small. Expand when it needs to.

    How ZipBuild Handles This Pattern

    When generating production-ready scaffolds, ZipBuild applies this exact structure. The generated codebase has clear feature boundaries, making it trivial for AI assistants to understand and extend. Your codebase stays maintainable from day one, not after a painful refactor at month six.

    Start Building with a Clear Structure

    The next time you create a Next.js project, resist the default structure. Spend 15 minutes setting up features/, shared/, and config/ folders. Define one feature (auth, or onboarding, or payments—whatever your app needs first). The structure will pay dividends the moment your project grows past a few thousand lines.

    Your future self will thank you. Your AI coding assistant will be more effective. Your team will move faster.

    Try the free discovery chat at zipbuild.dev to see how this structure looks in a fully generated SaaS application.

    Written by ZipBuild Team

    Ready to build with structure?

    Try the free discovery chat and see how ZipBuild architects your idea.

    Start Building