Featured-Sliced Design with Next.js
Featured-Sliced Design is an architectural methodology for front-end applications. This architecture defines rules and conventions to organize code, reducing coupling between different components of the application. FSD is a powerful architecture that uses the concept of the dependency rule to establish its layers, allowing us to write and maintain code with great ease. Let's explore how Featured-Sliced Design makes this possible with an example in Next.js.
Github repository: https://github.com/Caraya0100/fsd-nextjs
Folders Organization
FSD defines the organization of folders as follows:
Each layer in the project is represented by a folder. The second level of folders contains the Slices—think of a slice as a module. Their main purpose is to group code, but here's the trick: you can't import from other slices within the same layer. At the deepest level, we find the Segments. These group code by technical nature: .tsx files (ui), interfaces (model), utility functions (lib), and fetch functions (api).
But how can we prevent imports between slices? To understand this, we need to examine the definition of each layer.
Layers
App: This layer handles app-wide concerns. Here, you'll find elements like providers and global CSS styles. You can import components from all other layers.
Processes: Deprecated
Pages: This layer contains the pages of your application. Here, you'll use widgets, features, entities, and shared components.
Widgets: These are self-sufficient UI blocks composed of lower-level units like entities and features. Think of widgets this way: if you need to combine two features in a component, this is where that component should be created. In this layer, you can import from features, entities, and shared.
Features: This layer houses the application's features. It's where you'll place actions like getting a post, authenticating a user, or creating an order. You can import entities and shared components here.
Entities: These are typically the terms the business uses to describe the product—for example, posts, users, orders—but not exclusively. This layer contains the most important components specific to the application (you can only import from the Shared layer). If a component doesn't depend on any feature or widget, it should be placed here.
Shared: This is the most important layer. Its components are generic and not specific to the application. You can use them in other projects because they don't have any dependencies on other components of the application. You can literally move these folders to another project, install the necessary libraries, and use them.
As you can see, the dependencies between layers follow this pattern (the arrows indicate import statements):
app → pages → widgets → features → entities → shared
This concept is known as the dependency rule, where more important layers cannot depend on less important layers. If you need to import slice B from slice A in the same layer, it's likely that slice B is more important than initially thought and should be moved to an upper layer.
Note: Don't be misled by the image above. The shared layer is actually the most important (highest in the architecture), while the app layer is the least important (lowest in the architecture).
Time to code
We will make a home page for an economic journal. The page need to fetch a list of posts and categories, display a header with the title and description of the website, and a footer.
With these requirements, we can quickly define our slices, but let's focus only on the list of posts. From this, we'll have a slice called "post" in the entities layer, a "get-post" slice in the features layer, and a "posts-grid" slice in the widgets layer. How do we know if this structure is correct? It's all about importance. The "post" slice will likely be used by multiple slices in the features and widgets layers, so it must be placed in a higher layer of the architecture. The same goes for "get-post"—we might need to use it on other pages of the website, in widgets like related posts or sidebar sections.
You may wonder why not place the “post” slice in the shared layer. This is because is not generic, go to the “get-posts” segment and look at the code:
import { CmsApi, CmsModel } from '@shared/cms'
export async function getPosts(): Promise<CmsModel.Post[]> {
return await CmsApi.getPosts()
}
We are using a specific CMS for this website, which makes "post" a specific slice for this project. In contrast, the CMS API is actually the generic slice because it can be used in any project that needs this CMS.
The Layer Pages in Next.js
In recent versions of Next.js, the pages folder no longer exists. I recommend not using it in FSD either. Remember, in the pages layer, you build a page using widgets, features, entities, and shared components. This can be done seamlessly with the App Router structure, making it perfectly compatible with the FSD architecture.
Conclusion
The benefit of using Featured-Sliced Design in the front end lies in the importance hierarchy of each layer and the rules governing their dependencies. When a component is used by many other components, it signifies greater importance and should be placed in a higher layer—one that cannot import components from the same or lower layers. This approach reduces coupling both vertically and horizontally, enhancing the overall architecture.