Skip to content
← all notes

How to Structure a Large Next.js App (App Router, 2026)

The feature-based folder structure I use for large Next.js App Router projects, with the rules that keep it clean as the team and codebase grow.

How to Structure a Large Next.js App (App Router, 2026)

TL;DR: For a large Next.js App Router project, keep the app directory thin and use it only for routing. Put everything else in feature folders that own their components, server actions, queries, schemas and types. Keep shared UI in components/ui, infrastructure in lib, and make all data access server-only, with authentication checked next to the data. This article reflects Next.js 16 and React 19.

Next.js gives you flexible conventions but very few opinions about where your own code should live. That is fine for a small site and painful for an app with dozens of routes and a team of developers. This is the structure my team uses for large Next.js projects, and the rules that keep it clean.

Key takeaways

  • app/ is for routing. Pages should be short and mostly compose components.
  • Group code by feature, not by file type.
  • Use route groups for layouts and private folders for route-specific components.
  • Build a server-only data access layer and authorize there.
  • Treat every server action as a public API endpoint.

What does the folder structure look like?

Here is the full structure, using an HR app with leave management as the example:

src/
├── app/
│   ├── (marketing)/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── pricing/page.tsx
│   ├── (app)/
│   │   ├── layout.tsx
│   │   ├── dashboard/page.tsx
│   │   └── leaves/
│   │       ├── page.tsx
│   │       ├── [id]/page.tsx
│   │       └── _components/
│   │           └── leave-filters.tsx
│   ├── api/
│   │   └── webhooks/route.ts
│   ├── layout.tsx
│   ├── not-found.tsx
│   └── global-error.tsx
├── features/
│   └── leaves/
│       ├── components/
│       │   ├── leave-form.tsx
│       │   └── leave-table.tsx
│       ├── actions.ts
│       ├── queries.ts
│       ├── schema.ts
│       └── types.ts
├── components/
│   └── ui/
│       ├── button.tsx
│       └── dialog.tsx
├── lib/
│   ├── auth.ts
│   ├── db.ts
│   └── utils.ts
├── config/
│   └── env.ts
└── proxy.ts

The rest of this article explains each part.

Should you use a src folder in Next.js?

For large projects, yes. The src folder is optional, but it separates your application code from root-level config such as next.config.ts, tsconfig.json, eslint.config.mjs and package.json. As tooling grows, that separation keeps the project root readable.

What belongs in the app directory?

Only routing: pages, layouts, loading and error states, route handlers and metadata. A page should fetch what it needs and compose components. If a page file grows past a screen or two, logic is leaking into it.

// src/app/(app)/leaves/[id]/page.tsx
import { notFound } from 'next/navigation';

import { getLeaveRequest } from '@/features/leaves/queries';
import { LeaveDetails } from '@/features/leaves/components/leave-details';

type Props = { params: Promise<{ id: string }> };

export default async function LeavePage({ params }: Props) {
  const { id } = await params;
  const leave = await getLeaveRequest(id);

  if (!leave) notFound();

  return <LeaveDetails leave={leave} />;
}

Note that params is a Promise. Since Next.js 15, request-specific APIs such as params, searchParams, cookies() and headers() are asynchronous, so you await them.

How do route groups and private folders help?

Route groups are folders in parentheses, such as (marketing) and (app). They do not appear in the URL. Use them to give sections of the site different layouts: the marketing pages get a public header and footer, while the app pages get a sidebar and require login.

Private folders start with an underscore, such as _components. Next.js never turns them into routes, which makes them the right place for components used by only one route. When a second route needs the same component, move it into the feature folder.

Why organize by feature instead of by file type?

Because features change together. When the leave policy changes, you edit the leave form, the validation schema, the server action and the query. With a feature folder, all of those are in one place. With folders like components/, hooks/, services/ and types/ at the top level, one change touches four distant folders, and nobody knows which files can be deleted when a feature is removed.

Each feature folder follows the same shape:

  • components/ for UI specific to the feature
  • actions.ts for server actions that change data
  • queries.ts for server-only functions that read data
  • schema.ts for validation schemas shared by forms and actions
  • types.ts for the feature’s types

How should you structure data access?

Put all reads behind a server-only data access layer, and check authentication and authorization there, next to the data. This follows the Data Access Layer approach recommended in the Next.js security guidance.

// src/features/leaves/queries.ts
import 'server-only';
import { cache } from 'react';

import { db } from '@/lib/db';
import { getCurrentUser } from '@/lib/auth';

export const getLeaveRequest = cache(async (id: string) => {
  const user = await getCurrentUser();
  if (!user) throw new Error('Unauthorized');

  return db.leaveRequest.findFirst({
    where: { id, organizationId: user.organizationId },
  });
});

Two details matter here. The server-only package makes the build fail if this file is ever imported into a client component, so database code cannot leak into the browser. And React’s cache deduplicates calls within a single request, so several components can ask for the same data without extra queries.

Where should server actions live?

In the feature’s actions.ts, with "use server" at the top. Treat each action as a public endpoint: anyone can call it with any input, so it must authenticate and validate on its own.

// src/features/leaves/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

import { db } from '@/lib/db';
import { getCurrentUser } from '@/lib/auth';
import { leaveRequestSchema } from './schema';

export async function requestLeave(formData: FormData) {
  const user = await getCurrentUser();
  if (!user) throw new Error('Unauthorized');

  const input = leaveRequestSchema.parse(Object.fromEntries(formData));

  await db.leaveRequest.create({
    data: { ...input, userId: user.id, organizationId: user.organizationId },
  });

  revalidatePath('/leaves');
}

Sharing leaveRequestSchema between the form and the action means the client and server always agree on what valid input looks like.

What changed in Next.js 16 that affects structure?

  • middleware.ts is now proxy.ts. The file was renamed, and the exported function is called proxy. With a src folder, it lives at src/proxy.ts. Use it for redirects, rewrites and lightweight checks, but keep real authorization in your data access layer.
  • Turbopack is the default bundler for both development and production builds.
  • Caching is more explicit. With Cache Components enabled, you opt in to caching with the "use cache" directive instead of relying on implicit defaults, which makes it worth keeping data functions in predictable places like queries.ts.
// src/proxy.ts
import { NextResponse, type NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  const hasSession = request.cookies.has('session');

  if (!hasSession) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = { matcher: ['/dashboard/:path*', '/leaves/:path*'] };

Naming and import rules that keep it consistent

  • Kebab-case file names, such as leave-form.tsx. They avoid case-sensitivity bugs between macOS, Windows and Linux.
  • One import alias, @/ for src/, configured in tsconfig.json. No long relative paths.
  • Avoid large barrel files (index.ts files that re-export everything). They make it easy to create circular imports and to pull server code into client bundles by accident.
  • Validate environment variables at startup in config/env.ts, so a missing variable fails the build instead of a page at runtime.
  • Features do not import from each other’s internals. If two features need the same thing, move it to components/ui or lib.

When is this structure overkill?

For a landing page or a small marketing site, it is. Start with app/ and a components/ folder, and introduce feature folders when you notice one change touching many distant files. The structure should follow the size of the problem.

Frequently asked questions

What is the best folder structure for a large Next.js App Router project?

Keep the app directory thin and use it only for routing, then group everything else by feature: each feature folder holds its components, server actions, queries, validation schemas and types. Shared UI goes in components/ui and infrastructure such as the database client and auth goes in lib.

Should I use a src folder in Next.js?

It is optional, but for large projects I recommend it. A src folder separates application code from root-level config files like next.config.ts, package.json and tsconfig.json, which keeps the project root readable as tooling grows.

Where should server actions live in a Next.js project?

Put them in an actions.ts file inside the feature they belong to, marked with "use server" at the top. Every action should check authentication and validate its input itself, because server actions are public endpoints that anyone can call.

What replaced middleware.ts in Next.js 16?

Next.js 16 renamed middleware.ts to proxy.ts, and the exported function is named proxy. It still runs before a request is completed, so it suits redirects, rewrites and lightweight checks, but real authorization should still happen next to your data.

About the author

I’m Hardik Gohil, a Web Team Lead and Full-Stack Developer at ProLance IT in Surat, India. My team builds and maintains production Next.js applications, and this structure comes from what has worked, and what has not, across those projects. We enforce it during review with the code review checklist I use with my team.

— Hardik

filed under notes · Oct 2026 · thanks for reading

Keep reading

Back to all notes

Greetings from your inbox

Let's build something.

Hi Hardik, I've got an idea, a messy codebase, or just a question about…

To

Hardik Gohil

Surat, Gujarat, India

Write the message here, it lands in my inbox.

Write me a message