Next.js App Router and Feature-Sliced Design (FSD): Causes of `app`·`pages` Directory Conflicts and Isolation Strategies
I was quite thrown off at first when I created src/pages and Next.js treated it as a Pages Router. It looks like a problem caused by overlapping folder names, but in reality this conflict arises because the two systems have fundamentally different ways of looking at the file system. FSD categorizes folders by "what responsibility does this code have," while Next.js categorizes them by "what URL does this file handle." It's a collision of the same name, but different worldviews.
After reading this article, you'll be able to establish a directory structure that preserves all 6 FSD layers without touching Next.js's routing rules. There is only one key principle: keep the routing folder controlled by Next.js at the project root, and isolate the entire set of FSD layers under src/. On top of this principle, we'll walk through real code covering Provider wiring, Server Action placement, and cache tag management.
If FSD is unfamiliar to you, it's recommended to first skim the official documentation's Introduction. This article focuses on integration with the Next.js App Router, assuming you already know the basic concepts of layers, slices, and segments.
Core Concepts
How FSD Sees Code
FSD is an architectural methodology that organizes frontend applications into layers by responsibility. Code is structured in the following 6-layer order, and the core rule is a unidirectional dependency where upper layers can only import from lower layers.
app → pages → widgets → features → entities → shared
(upper, more dependencies) (lower, fewer dependencies)The arrows here represent "import direction." app can import pages, but pages importing app is a rule violation. In FSD, the import direction and the dependency direction are the same — follow the arrows and that's the direction dependencies flow.
FSD layers in one line:
appis the app entry point (Providers, global config),pagesis page-level UI composition,widgetsare independent UI blocks,featuresare units of user interaction,entitiesare domain models, andsharedis reusable utilities.
One thing worth knowing in advance: if your team is of any size, introducing Steiger, the official FSD architecture linter, into your CI will automatically catch incorrect imports between layers. We'll cover how to install it at the end of this article, so keep in mind that "automation is possible later" as you read along.
Naming Conflicts with Next.js: Why It's Not Simply a Folder Name Problem
The two systems use the same folder names with completely different meanings.
| Folder Name | Meaning in FSD | Meaning in Next.js |
|---|---|---|
app/ |
Top-level entry layer (Providers, global config) | App Router routing root |
pages/ |
Page-level UI composition layer | Pages Router routing root (legacy) |
Next.js treats app/ and pages/ at the project root as reserved routing directories. So if you place the FSD pages layer in src/pages, Next.js recognizes it as a Pages Router, causing builds to break or the two routers to conflict. This isn't because the names overlap — it's a conflict that arises because the two systems interpret the file system from different perspectives.
Isolation Strategy: Recommended Directory Structure
The solution is surprisingly simple. Place the routing folder controlled by Next.js at the project root, and gather the entire set of FSD layers under src/.
project-root/
├── app/ # Next.js App Router (routing only, keep as thin as possible)
│ ├── layout.tsx
│ ├── (auth)/
│ │ └── login/
│ │ └── page.tsx # Only re-exports src/pages/login
│ └── dashboard/
│ └── page.tsx
├── pages/ # ★ Empty folder — even a single .gitkeep is fine
│ └── .gitkeep
└── src/
├── app/ # FSD app layer (Providers, global styles, i18n)
│ ├── providers/
│ └── styles/
├── pages/ # FSD pages layer
├── widgets/
├── features/
├── entities/
└── shared/Let's clarify why an empty pages/ folder is needed at the root. According to the Next.js official documentation, using src/app activates App Router even without app/ at the root. The behavior where an empty root pages/ prevents src/pages from being recognized as a Pages Router is more of a community-observed behavior than something officially documented. Before applying this to a real project, it's recommended to check the latest behavior in the FSD official Next.js guide.
⚠️ Caution: If someone thinks "this folder isn't being used" and deletes this empty
pages/folder,src/pageswill be recognized as a Next.js Pages Router. Sharing within the team why this folder is needed along with.gitkeepcan help prevent accidents.
Project Scale Where This Structure Fits
Honestly, FSD is not a structure that suits every project. If you have fewer than 20 features, or if it's a project being built in a short timeframe by yourself or a small team, the cost of having the entire team internalize the layer·slice·segment concepts may outweigh the benefits. If features are steadily growing, you have 3 or more team members, and you plan to maintain the codebase for over a year — that's when FSD's advantages start to exceed the initial investment.
Practical Application
Example 1: Connecting the FSD app Layer from app/layout.tsx
Why is this pattern necessary? React's Context API only works within a client tree. Server Components are rendered on the server as static output, so there is no persistent React component tree, and the mechanism for subscribing to or propagating Context values does not work. Therefore, Providers that create Context — like QueryClientProvider or ThemeProvider — must be inside Client Components declared with 'use client'.
Keeping app/layout.tsx as thin as possible and delegating actual Provider composition to src/app/providers/ lets you manage this boundary clearly.
// src/app/providers/index.tsx
'use client'; // Required because Context does not work in Server Components
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider attribute="class">{children}</ThemeProvider>
</QueryClientProvider>
);
}// app/layout.tsx — Next.js routing layer, no business logic
import { Providers } from '@/app/providers';
import '@/app/styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Establishing a team convention of only declaring 'use client' inside src/app/providers/ makes it clear where the boundary between Server Components and Client Components lies.
Example 2: Connecting the FSD pages Layer to a Next.js Route
If you start putting data fetching or state management directly in app/(route)/page.tsx files, the FSD structure will break down quickly. Ideally, routing files should only decide "what component to show at this URL," delegating actual UI composition to slices in src/pages/.
// src/pages/product-list/index.tsx — FSD pages layer
import { ProductFilters } from '@/features/product-filter';
import { ProductGrid } from '@/widgets/product-grid';
export function ProductListPage() {
return (
<main>
<ProductFilters />
<ProductGrid />
</main>
);
}// app/products/page.tsx — handles routing only, one-line re-export
export { ProductListPage as default } from '@/pages/product-list';This way, even if the routing structure changes later, you won't need to touch the src/pages/ slices.
Note: The
export { X as default }syntax works fine in Next.js, but there have been reports of issues in some Turbopack environments. Depending on your team's environment, the direct declaration styleexport default function Page() { return <ProductListPage />; }may be safer.
Example 3: Connecting a Server Action to the Pattern from Example 2
If src/pages/login/ from Example 2 composes a LoginForm, where should the authentication mutation logic live? Since the subject of the authentication user interaction is the features/auth slice, placing the Server Action inside that slice aligns with FSD principles. Cache invalidation (revalidateTag) responsibility is also co-owned by that domain.
// src/entities/user/api/tags.ts — manage cache tags as constants
export const USER_CACHE_TAG = 'user' as const;// src/features/auth/api/login-action.ts
'use server';
import { redirect } from 'next/navigation';
import { revalidateTag } from 'next/cache';
import { authenticate } from '@/entities/user/api'; // actual auth API call
import { USER_CACHE_TAG } from '@/entities/user/api/tags';
export async function loginAction(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
await authenticate({ email, password });
revalidateTag(USER_CACHE_TAG);
redirect('/dashboard');
}// src/features/auth/ui/LoginForm.tsx
import { loginAction } from '../api/login-action';
export function LoginForm() {
return (
<form action={loginAction}>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Login</button>
</form>
);
}Cache tag pattern: Hardcoding the strings passed to
revalidateTagmakes typos and mismatches more likely down the line. Exporting them as constants fromentities/{slice}/api/tags.tsand importing from there improves both type safety and traceability.
Example 4: Alternative pagesLayer Naming Strategy
Continuously managing an empty pages/ folder at the root may feel cumbersome for some teams. Renaming the FSD pages layer folder itself is also covered in the FSD official documentation.
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/app/*": ["./src/app/*"],
"@/pages/*": ["./src/pagesLayer/*"], // actual folder name is pagesLayer
"@/widgets/*": ["./src/widgets/*"],
"@/features/*": ["./src/features/*"],
"@/entities/*": ["./src/entities/*"],
"@/shared/*": ["./src/shared/*"]
}
}
}Creating the actual folder as src/pagesLayer/ while keeping the paths alias in tsconfig as @/pages/* lets you use the same import paths in code while avoiding Next.js conflicts. However, when following "Go to definition" in your IDE, the pagesLayer folder will open. Since the path visible in code (@/pages/...) differs from the actual folder name (pagesLayer), it's good to share this with new team members in advance.
Pros and Cons Analysis
Pros
| Item | Details |
|---|---|
| Clear dependency direction | Structurally prevents circular dependencies via unidirectional upper → lower imports |
| Team autonomy | Teams can work independently per slice, minimizing conflicts |
| Easy code navigation | Location is immediately predictable by "which layer, which slice" |
| RSC-friendly | Server Component by default, with clearly defined Client boundaries per layer |
| Scalability | Minimal impact on existing code when adding features |
In practice, you typically feel these benefits most acutely during onboarding. When a new team member asks "where is the code for this feature?", the conversation ends with "check the features/auth slice." Even someone seeing the codebase for the first time can predict the overall structure to some degree just from the layer names.
Cons and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Overkill for small projects | With fewer than 20 features, boilerplate cost exceeds the benefit | Evaluate adoption once scale warrants it |
| Learning curve | The entire team must internalize layers·slices·segments to maintain consistency | Prepare team onboarding docs, auto-validate with Steiger |
| Next.js naming conflicts | Frequent initial setup mistakes: managing empty root pages/, aligning tsconfig paths, etc. |
Set up as a project template in advance |
| RSC ↔ Client boundary confusion | Context Providers must be Client Components | Establish convention of only declaring 'use client' in src/app/providers/ |
| Excessive slice splitting | Splitting into too-small units makes the entities/features boundary ambiguous, causing team debates | Apply the rule: "user-triggered interactions are features, domain data models are entities" |
Most Common Mistakes in Practice
-
Deleting the empty root
pages/folder: Thinking "this folder isn't being used" and deleting it causessrc/pagesto be recognized as Next.js Pages Router. Leaving a comment or note inREADMEexplaining why this folder must exist can help prevent teammates from accidentally deleting it. -
Writing business logic in
app/routing files: Starting to put data fetching or state management directly inapp/dashboard/page.tsxwill quickly break the FSD structure. A routing file ideally consists of a single re-export line. -
Missing
'use client'insrc/app/providers/files: Providers that create Context must be Client Components. If rendered on the server, Context will not work and a runtime error will occur. It's good practice to clearly declare'use client'in each Provider file.
Closing
The core principle of using Next.js App Router alongside FSD is one thing: Next.js routing at the project root, the entire FSD layer set isolated inside src/. Establishing this structure from the start means the question "where should I put this?" disappears even as features grow. The entire team predicts code locations by the same standard, and you'll notice a significant reduction in the conversational overhead spent on reviews and onboarding.
Three steps you can start with right now:
-
Set up the directory structure: Create
app,pages,widgets,features,entities, andsharedfolders undersrc/, and createpages/.gitkeepat the project root. Setting up per-layer aliases intsconfig.json'spathsat this point will make subsequent work easier. -
Connect
app/layout.tsx→src/app/providers/: Move Provider code that was previously written directly inlayout.tsxtosrc/app/providers/index.tsx, add the'use client'declaration, and transition to a structure wherelayout.tsximports from it. -
Install Steiger and integrate with CI: Install with
pnpm add -D steigerand add thesteiger ./srccommand to your CI pipeline to start automatically catching incorrect imports between layers. Before installing, it's recommended to check the current package name and plugin configuration in the official repository — the package structure may have been updated.
References
- Feature-Sliced Design Official — Using with Next.js
- Feature-Sliced Design Official Blog — The Ultimate Next.js App Router Architecture
- DEV Community — How to deal with NextJS App Router and FSD problem
- HackerNoon — How to Fix the NextJS App Router and FSD Problem
- GitHub — feature-sliced/steiger (Official FSD Architecture Linter)
- GitHub — yunglocokid/FSD-Pure-Next.js-Template
- Next.js Official — src Folder Convention
- Next.js Official — Project Structure
- StackBlitz — Using Next.js App Router with FSD (Live Example)