Illustration of stacked layers representing Feature-Sliced Design

Feature-Sliced Design in a Next.js App Router codebase

App Router gives you routing, not architecture. A pragmatic mapping of FSD layers onto app/, server components, and shared UI — including the parts worth skipping.

The App Router answers where a URL renders. It has no opinion about where a feature lives, and that is the question that decides whether a codebase is still pleasant to work in after a year.

The default outcome is familiar: components/ grows to two hundred files, half of them used once, and a utils.ts that everything imports and nobody can safely change. Feature-Sliced Design is one answer — a layered convention with an explicit rule about which direction imports may point. This site is built with it, so the examples below are the real structure rather than a diagram.

The layers, and the one rule that matters

FSD stacks a codebase into layers and allows imports in one direction only: downward. A widget may import a feature; a feature may never import a widget.

txt
src/
├── app/        # routes, layouts, metadata — Next.js owns this folder
├── widgets/    # self-contained page sections (nav, hero, article)
├── features/   # user-facing capabilities with state (chat, search)
├── entities/   # domain objects and their content (post, user)
└── shared/     # ui kit, api client, i18n, config, hooks

That single constraint is what produces the useful properties. Deleting a feature cannot break a widget it never knew about. shared importing nothing means it stays genuinely reusable instead of quietly becoming a second home for business logic. And an import that points the wrong way is a mechanical, reviewable mistake rather than a matter of taste.

One naming collision to be aware of: canonical FSD calls its top layer app and means application setup — providers, global styles. Next.js also calls its routing folder app. In practice the Next.js meaning wins, because the framework requires the name; application setup lives in the root layout and in shared/config. Not worth fighting.

Keep route files thin

A page.tsx should read like a table of contents: resolve params, resolve locale, export metadata, render a widget. Once a route file contains layout decisions, it is no longer possible to reuse that screen anywhere — and in a localized site "anywhere" arrives immediately, because the same screen exists under two URL prefixes.

tsx
// app/blog/[slug]/page.tsx
export const dynamicParams = false;

export const generateStaticParams = async () =>
  getPosts().map((post) => ({ slug: post.slug }));

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const post = getPost(slug);

  if (!post) notFound();

  return (
    <main className="main">
      <PostArticle post={post} locale={DEFAULT_LOCALE} />
    </main>
  );
}

The German route is then the same six lines with a different locale constant. The screen itself — header, meta rail, body, related posts — has one implementation, and translation is a parameter rather than a fork. Two thin route files are much cheaper to maintain than one clever shared one.

Server components move the boundary, not the structure

The most useful thing FSD does in an App Router project is make the client boundary visible. Interactivity is a property of a specific piece of UI, and slices give you an obvious place to put that piece.

  • `entities` stay pure data and types. No React, no use client, no fetching. This layer should be importable from a route handler, a sitemap generator, or an RSS feed without pulling in a rendering runtime.
  • `widgets` are server components by default. A blog article renders as static HTML, which is what search engines and slow connections both want.
  • `features` are where state lives, and therefore where use client usually belongs — but on the leaf, not the wrapper.
  • `shared/ui` stays presentational. A shared component that reaches into a store is no longer shared, it just has not noticed yet.

A concrete example of the leaf rule: this article page has exactly one interactive element, a copy-link button. Marking the whole article use client to get it would ship the entire body renderer to the browser for one navigator.clipboard call. As its own client component inside a server-rendered widget, the cost is a few hundred bytes. This is the mechanism behind most of the advice in performance budgets that survive a real roadmap.

Every slice has a public API

Each slice exposes an index.ts and everything else is internal. This is the part teams tend to skip, and it is the part that does the most work.

ts
// entities/post/index.ts
export * from './model/types';
export {
  getPosts,
  getPost,
  getRelatedPosts,
  getReadingMinutes,
  formatPostDate,
  getPostPath,
} from './model/selectors';

Two things follow. Refactoring inside a slice stops being a cross-cutting change — move files, rename functions, split modules, and no consumer notices. And the export list becomes a design review in itself: a slice whose index.ts exports thirty symbols is telling you it is actually two slices.

You can enforce the direction of imports with eslint-plugin-boundaries or a path restriction rule. On a small codebase, a documented convention and code review are enough. On anything with more than a few contributors, make the linter do it — architectural rules that depend on everyone remembering them decay at exactly the speed you would expect.

What to skip

Applied literally, FSD generates a lot of directories. The version worth keeping is smaller than the specification.

  • Do not create every segment up front. ui, model, lib, api, config per slice is a template, not a requirement. Add a segment when it has a second file.
  • Do not add an `entity` for something with no behaviour. A type in shared is fine. Entities earn their place when they have selectors, invariants, or content of their own.
  • Do not build `processes`. The layer exists in older versions of the specification and, in my experience, becomes a folder where things go when nobody wants to decide.
  • Do not slice a five-page site. Structure has a fixed cost. Below a certain size, app plus shared plus a handful of components is the correct architecture, and adding layers is just ceremony.

FSD is not the only workable answer, and it is not the interesting part of any product. What it does is remove a recurring argument about where code goes, which is worth a surprising amount when a codebase outlives the context that produced it. The source for this site is public if you want to see the layering applied to something real, including the blog you are reading.

Feature-Sliced Design in a Next.js App Router codebase — Vladyslav Dobrodii