Back to blog
·6 min read

How to Structure a Next.js 15 Project for Scalable SaaS Apps Without Getting Lost

Growing Next.js projects turn into chaos fast when you organize by file type. This guide shows you how to structure a scalable Next.js 15 codebase using feature-based architecture so your team can actually ship features instead of fighting with folder chaos.

The Problem: Your Next.js Project is Already Breaking

You started with a clean app/ directory. Three months later you've got 200+ files crammed into a components folder, a utils.ts file that's ballooned to 2,000 lines, and nobody on your team knows where anything lives anymore.

This happens to almost every developer building a Next.js application at scale. You follow the framework's conventions—throw components in components/, hooks in hooks/, utils in utils/—and it works great for a weekend project. But the moment you're building something real, something that needs to handle complex data flows, multiple user dashboards, or an expanding feature set, that flat structure collapses.

The problem isn't Next.js. The problem is organizing by type instead of by feature. When your folder structure doesn't match how your features actually work, everything gets harder: onboarding new developers takes longer, features take longer to ship, and refactoring becomes terrifying because you can't trace dependencies.

If you're building a SaaS application with Claude Code or any AI-assisted development tool, this problem gets worse because those tools can't understand implicit folder conventions. They need your codebase to be explicitly organized so prompts can find what's relevant.

Feature-Based Architecture: What It Actually Means

Feature-based organization groups all code related to a specific business capability into one place. Instead of spreading a feature across multiple type-based folders, you keep the route, components, hooks, utilities, types, and database queries for that feature together.

Here's what this looks like in a real Next.js 15 project with the App Router:

```

src/

app/

page.tsx (landing page)

layout.tsx

api/

health/

route.ts

features/

auth/

components/

LoginForm.tsx

SignupForm.tsx

hooks/

useAuth.ts

useSession.ts

services/

authService.ts

types/

auth.types.ts

app/

login/

page.tsx

signup/

page.tsx

forgot-password/

page.tsx

dashboard/

components/

DashboardHeader.tsx

UserStatsCard.tsx

DataTerminal.tsx

hooks/

useDashboardData.ts

useUserMetrics.ts

services/

dashboardService.ts

metricsService.ts

types/

dashboard.types.ts

app/

page.tsx

settings/

page.tsx

billing/

components/

PricingCard.tsx

SubscriptionForm.tsx

hooks/

useSubscription.ts

services/

billingService.ts

types/

billing.types.ts

app/

page.tsx

success/

page.tsx

shared/

components/

Button.tsx

Modal.tsx

Header.tsx

hooks/

useLocalStorage.ts

useMediaQuery.ts

services/

apiClient.ts

types/

common.types.ts

utils/

formatting.ts

validation.ts

```

The key principle: if you're building a feature and need something, it should either live in that feature's folder or in shared/.

Why This Structure Scales Better

When you're building the authentication system, everything you need is in features/auth/. Your LoginForm component, your useAuth hook, your types, your API calls—they're all in one place. You can understand the entire feature's scope without hunting through five different folders.

This matters even more when you're using AI to help build features. When you prompt Claude Code or Cursor with context, you can easily include an entire feature folder. The AI understands the complete picture of what you're building instead of trying to infer connections between scattered files.

As your team grows, this becomes critical. A new developer can jump into a feature, and everything they need to understand it is in one place. They're not asking "where do hooks go?" or "which utils file should I check?" The structure answers those questions implicitly.

Shared Components vs Feature Components

Not everything belongs in a feature folder. The Shared pattern handles:

  • Generic UI primitives (buttons, modals, forms, cards)
  • Cross-cutting utilities (date formatting, API helpers, validation)
  • Custom hooks that multiple features use (useMediaQuery, useLocalStorage)
  • Common types and interfaces
  • If a component is only used within one feature, it lives in that feature folder. If three features need it, it moves to shared/. This prevents shared/ from becoming a dumping ground while keeping your feature folders focused.

    Organizing Routes with the App Router

    The App Router in Next.js 15 plays nicely with this structure. Your routes can live both in the traditional app/ directory and within feature folders.

    For public routes that don't belong to a specific feature (landing page, status page, etc.), keep them in app/. For feature-specific routes, duplicate the folder structure inside your feature. So features/auth/app/login/page.tsx maps to the route /login.

    Then in your root app/layout.tsx, you can import and compose layouts from features as needed. This keeps route organization clean while keeping all auth-related code together.

    Database and Service Layer Patterns

    Inside features, your services/ folder handles:

  • Database queries (using your ORM, Supabase client, or query builder)
  • External API calls
  • Business logic that doesn't belong in components
  • For features/billing/services/billingService.ts, you might have:

    ```

    export const createSubscription = async (userId: string, planId: string) => {

    // Call your database or billing API

    }

    export const getActiveSubscription = async (userId: string) => {

    // Query user's current subscription

    }

    export const cancelSubscription = async (subscriptionId: string) => {

    // Handle cancellation logic

    }

    ```

    These services get imported by your components and hooks, keeping the separation between data handling and UI logic clean.

    State Management Across Features

    With this structure, you'll naturally move away from global state management bloat. Most state belongs in the feature that uses it. React Query, SWR, or client-side context for feature-specific state work well here.

    For truly cross-feature state (authenticated user info, theme, etc.), keep a minimal shared/stores/ folder with those specific concerns. But resist the urge to put everything there. Each feature should manage its own state.

    The Real Win: Scaling Without Pain

    This structure doesn't just make code easier to find. It makes your codebase scale linearly instead of exponentially. A 100-feature codebase with this structure stays manageable. A 100-feature codebase where everything is scattered by type becomes a nightmare.

    When you're building SaaS applications, especially when you're using AI tools to accelerate development, this matters. You can build faster when your code is organized in a way that actually reflects how the product works.

    Getting Started with This Structure

    If you're starting a new Next.js project, begin with this structure from day one. If you're refactoring an existing project, pick one feature, reorganize it, and use it as a template for the rest.

    The initial time investment pays back quickly. Your first refactored feature will feel noticeably easier to work with than the type-based version. That feeling spreads to your team.

    For teams building production SaaS applications and wanting a head start, platforms like ZipBuild scaffold this exact structure automatically, setting you up with feature-based organization, proper typing, and database integration from the start.

    Try the free discovery chat at zipbuild.dev to see how proper project structure accelerates your development timeline.

    Written by ZipBuild Team

    Ready to build with structure?

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

    Start Building