Skip to content

Instantly share code, notes, and snippets.

@makp0
Created July 10, 2025 15:23
Show Gist options
  • Save makp0/d99eaed6eeb21bd82bcd8d3f3aa3785d to your computer and use it in GitHub Desktop.
Save makp0/d99eaed6eeb21bd82bcd8d3f3aa3785d to your computer and use it in GitHub Desktop.
Feature Sliced Design Plain Doc

Crawled Documentation

Welcome | Feature-Sliced Design

Source: https://feature-sliced.design/

Features

Explicit business logic

Easily discoverable architecture thanks to domain scopes

Adaptability

Architecture components can be flexibly replaced and added for new requirements

Tech debt & Refactoring

Each module can be independently modified / rewritten without side effects

Explicit code reuse

A balance is maintained between DRY and local customization

Concepts

Public API

Each module must have a declaration of its public API at the top level

Isolation

The module should not depend directly on other modules of the same layer or overlying layers

Needs Driven

Orientation to business and user needs

Scheme

Companies using FSD

FSD is used in your company? Tell us

Feature-Sliced Design: Concept and Importance

Feature-Sliced Design is a modern architectural methodology tailored for building scalable front-end applications, particularly in the React ecosystem. The core principle is to organize project structure by focusing on application features rather than purely technical layers. In traditional paradigms, developers often group code by technical concernsβ€”such as components, services, or stylesβ€”leading to monolithic files and complicated dependencies. Feature-Sliced Design addresses these issues by placing features at the forefront. This approach promotes a cleaner, more intuitive structure that streamlines collaboration and maintenance. From an EEAT standpoint (Expertise, Experience, Authoritativeness, and Trustworthiness), Feature-Sliced Design demonstrates its value by emphasizing best practices that are well-recognized within the React community. Its feature-centered mindset ensures each piece of functionality remains coherent and easier to extend, contributing to a trustworthy and robust codebase.

History and Origin

The roots of Feature-Sliced Design can be traced back to ongoing community discussions about how to effectively manage complexity in large-scale front-end projects. Developers realized that standard file-grouping methods, whether by type or layer (for instance, "components," "containers," "services"), often failed to scale gracefully. Over time, the concept evolved through collaborative input from open-source contributors and thought leaders who recognized the need for a more intuitive and business-oriented approach. Although Feature-Sliced Design shares conceptual similarities with domain-driven design and modular architectures, it is specifically tailored to tackle React's component-driven nature, ensuring that complexities remain compartmentalized and that boundaries are clearly defined.

Key Elements of Feature-Sliced Design

At the heart of this methodology are distinct layers, slices, and segments. Each layer groups the application's parts in a way that reflects the user's journey and the business logic rather than technical minutiae. Slices break down major features into smaller domains, and segments provide clarity within those slices, capturing subtasks or subcomponents in a logically separated manner. By structuring a project with these elements, developers create code that is more approachable, testable, and easier to onboard new team members into.

Benefits for Large-Scale React Projects

One of the major advantages in adopting Feature-Sliced Design is that it naturally accommodates growth. Large-scale React applications often suffer from tangled dependencies as features expand. By centering each part of the application on specific features, it becomes far simpler to isolate bugs, roll out new functionality, and refactor existing code. This architectural clarity drives higher maintainability and reduces the likelihood of regressions. From a team perspective, individuals can work in parallel on different slices without stepping on each other's toes, accelerating both feature development and knowledge sharing across the organization.

When to Apply Feature-Sliced Design

Although suitable for projects of various sizes, Feature-Sliced Design truly shines when an application begins to scale beyond a few simple pages or components. If your React project is rapidly growing or when multiple teams are collaborating, it is prudent to apply Feature-Sliced Design. This structured approach provides a robust foundation that can gracefully adapt to new requirements. Even in smaller projects, adopting at least some of its principles can help maintain a tidy and future-proof codebase.

Comparison with Traditional Architectures (MVC, MVP)

In frameworks such as MVC (Model-View-Controller) or MVP (Model-View-Presenter), functionality is commonly separated by layers of responsibility. While these approaches have been foundational in software development, they don't always align neatly with modern, component-based libraries like React. Feature-Sliced Design, by contrast, goes a step further in aligning the entire codebase around features and user flows, making it more intuitive when dealing with reusable components and complex state management. In essence, MVC or MVP can sometimes force front-end developers to mold their code around classical backend-centric architecture, whereas Feature-Sliced Design embraces the patterns that best suit React's inherently modular design.

Distinguishing Feature-Sliced from Atomic Design

Atomic Design focuses on building interfaces from the smallest possible elements (atoms) up to more complex structures (molecules, organisms, templates, and pages). Feature-Sliced Design also embraces modularity but is primarily driven by business logic and user features. While Atomic Design is powerful for creating reusable UI libraries, Feature-Sliced Design explicitly addresses how features relate to each other and to the overall application. Both can coexist in a single project, but their focal points are different: Atomic Design is primarily concerned with UI consistency and reusability, whereas Feature-Sliced centers around application flow and domain complexity.

Common Terms (Layer, Slice, Segment)

Layers encapsulate the architectural tiers that group related parts of the application. In many examples, these layers might be labeled as apps, processes, pages, features, entities, and shared. Slices are the conceptual boundaries for distinct features, ensuring each set of functionalities or modules stands on its own. Within those slices, segments represent more granular subdivisions for organizing components, logic, and utilities specific to that particular feature. These terms collectively form a mental model that keeps the application tidy and easy to navigate.

Initial Difficulties and Learning Curve

Developers coming from more traditional file- or component-based structures may find it challenging to adopt Feature-Sliced Design. The primary learning curve often involves training one's mindset to think in terms of features rather than simply components or services. Configuring the directory layout can also seem daunting at first, especially when deciding how to slice the application's functionalities. Nonetheless, once these initial hurdles are overcome, teams usually discover that Feature-Sliced Design fosters a more maintainable and intuitive workflow.

Case Study: Building a Simple To-Do App with Feature-Sliced Design

Imagine a small to-do application with features like adding tasks, marking tasks complete, and filtering active or completed items. Under Feature-Sliced Design, each featureβ€”task creation, task completion, and task filteringβ€”would be its own slice, containing components, logic, and styles specifically for that function. The layers would separate application-level concerns (like user authentication or routing) from shared utilities and UI elements. Each slice might have its own local state management for tasks, which can be lifted or shared at the higher layers if global state becomes necessary. Although the app remains simple, the immediate benefit is clear: any developer joining the project can quickly see where each functionality resides and how it interacts with the rest of the application, eliminating guesswork and streamlining future enhancements. In summary, Feature-Sliced Design presents a practical and forward-looking approach to structuring React projects. By prioritizing features as the foundational building blocks, it offers clarity and scalability that traditional architectures often lack. Whether you are building a small side project or architecting an enterprise-level platform, incorporating Feature-Sliced Design principles can be a catalyst for more organized development, smoother collaboration, and a codebase that stands the test of time.

Documentation | Feature-Sliced Design

Source: https://feature-sliced.design/docs

Feature-Sliced Design (FSD) is an architectural methodology for scaffolding front-end applications. Simply put, it's a compilation of rules and conventions on organizing code. The main purpose of this methodology is to make the project more understandable and structured in the face of ever-changing business requirements. Feedback

🍰 About | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about

BACKGROUND-ORIENTED General information about methodology, team, community and development history

Main

Feedback

πŸ“š Reference | Feature-Sliced Design

Source: https://feature-sliced.design/docs/reference

A detailed description of the key concepts of Feature-Sliced Design. Feedback

🎯 Guides | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides

PRACTICE-ORIENTED Practical guides and examples on the use of Feature-Sliced Design. There is also describe migration guides and a handbook of harmful practices. It is most useful when you are trying to implement something specific or want to look at the methodology "in battle"

Main

Feedback

πŸš€ Get Started | Feature-Sliced Design

Source: https://feature-sliced.design/docs/get-started

Welcome! This section helps you to get acquainted with the application of Feature-Sliced Design and the basics of the methodology. You will also understand the key advantages of the methodology and the reasons for its creation. Feedback

Overview | Feature-Sliced Design

Source: https://feature-sliced.design/docs/get-started/overview

On this page Feature-Sliced Design (FSD) is an architectural methodology for scaffolding front-end applications. Simply put, it's a compilation of rules and conventions on organizing code. The main purpose of this methodology is to make the project more understandable and stable in the face of ever-changing business requirements. Apart from a set of conventions, FSD is also a toolchain. We have a to check your project's architecture, through a CLI or IDEs, as well as a rich library of .

Is it right for me?

FSD can be implemented in projects and teams of any size. It is right for your project if:

  • You're doing frontend (UI on web, mobile, desktop, etc.)
  • You're building an application, not a library

And that's it! There are no restrictions on what programming language, UI framework, or state manager you use. You can also adopt FSD incrementally, use it in monorepos, and scale to great lengths by breaking your app into packages and implementing FSD individually within them. If you already have an architecture and you're considering a switch to FSD, make sure that the current architecture is causing trouble in your team. For example, if your project has grown too large and inter-connected to efficiently implement new features, or if you're expecting a lot of new members to join the team. If the current architecture works, maybe it's not worth changing. But if you do decide to migrate, see the section for guidance.

Basic example

Here is a simple project that implements FSD:

  • πŸ“ app
  • πŸ“ pages
  • πŸ“ shared

These top-level folders are called layers. Let's look deeper: Folders inside πŸ“‚ pages are called slices. They divide the layer by domain (in this case, by pages). Folders inside πŸ“‚ app, πŸ“‚ shared, and πŸ“‚ pages/article-reader are called segments, and they divide slices (or layers) by technical purpose, i.e. what the code is for.

Concepts

Layers, slices, and segments form a hierarchy like this: Pictured above: three pillars, labeled left to right as "Layers", "Slices", and "Segments" respectively. The "Layers" pillar contains seven divisions arranged top to bottom and labeled "app", "processes", "pages", "widgets", "features", "entities", and "shared". The "processes" division is crossed out. The "entities" division is connected to the second pillar "Slices" in a way that conveys that the second pillar is the content of "entities". The "Slices" pillar contains three divisions arranged top to bottom and labeled "user", "post", and "comment". The "post" division is connected to the third pillar "Segments" in the same way such that it's the content of "post". The "Segments" pillar contains three divisions, arranged top to bottom and labeled "ui", "model", and "api".

Layers

Layers are standardized across all FSD projects. You don't have to use all of the layers, but their names are important. There are currently seven of them (from top to bottom):

  1. App β€” everything that makes the app run β€” routing, entrypoints, global styles, providers.
  2. Processes (deprecated) β€” complex inter-page scenarios.
  3. Pages β€” full pages or large parts of a page in nested routing.
  4. Widgets β€” large self-contained chunks of functionality or UI, usually delivering an entire use case.
  5. Features β€” reused implementations of entire product features, i.e. actions that bring business value to the user.
  6. Entities β€” business entities that the project works with, like user or product.
  7. Shared β€” reusable functionality, especially when it's detached from the specifics of the project/business, though not necessarily.

warning Layers App and Shared, unlike other layers, do not have slices and are divided into segments directly. However, all other layers β€” Entities, Features, Widgets, and Pages, retain the structure in which you must first create slices, inside which you create the segments. The trick with layers is that modules on one layer can only know about and import from modules from the layers strictly below.

Slices

Next up are slices, which partition the code by business domain. You're free to choose any names for them, and create as many as you wish. Slices make your codebase easier to navigate by keeping logically related modules close together. Slices cannot use other slices on the same layer, and that helps with high cohesion and low coupling.

Segments

Slices, as well as layers App and Shared, consist of segments, and segments group your code by its purpose. Segment names are not constrained by the standard, but there are several conventional names for the most common purposes: Usually these segments are enough for most layers, you would only create your own segments in Shared or App, but this is not a rule.

Advantages

Incremental adoption

If you have an existing codebase that you want to migrate to FSD, we suggest the following strategy. We found it useful in our own migration experience.

  1. Start by slowly shaping up the App and Shared layers module-by-module to create a foundation.
  2. Distribute all of the existing UI across Widgets and Pages using broad strokes, even if they have dependencies that violate the rules of FSD.
  3. Start gradually resolving import violations and also extracting Entities and possibly even Features.

It's advised to refrain from adding new large entities while refactoring or refactoring only certain parts of the project.

Next steps

  • Want to get a good grasp of how to think in FSD? Check out the .
  • Prefer to learn from examples? We have a lot in the section.
  • Have questions? Drop by our and get help from the community.

Feedback

Alternatives | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/alternatives

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned! History of architecture approaches

Big Ball of Mud

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

What is it; Why is it so common; When it starts to bring problems; What to do and how does FSD help in this

Smart & Dumb components

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

About the approach; About applicability in the frontend; Methodology position About obsolescence, about a new view from the methodology Why component-containers approach is evil?

Design Principles

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

What are we talking about; FSD position SOLID, GRASP, KISS, YAGNI, ... - and why they don't work well together in practice And how does it aggregate these practices

DDD

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

About the approach; Why does it work poorly in practice What is the difference, how does it improve applicability, where does it adopt practices

Clean Architecture

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

About the approach; About applicability in the frontend; FSD position How are they similar (to many), how are they different

Frameworks

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

About applicability in the frontend; Why frameworks do not solve problems; why there is no single approach; FSD position Framework-agnostic, conventional-approach

Atomic Design

What is it?

In Atomic Design, the scope of responsibility is divided into standardized layers. Atomic Design is broken down into 5 layers (from top to bottom):

  1. pages - Functionality similar to the pages layer in FSD.
  2. templates - Components that define the structure of a page without tying to specific content.
  3. organisms - Modules consisting of molecules that have business logic.
  4. molecules - More complex components that generally do not contain business logic.
  5. atoms - UI components without business logic.

Modules at one layer interact only with modules in the layers below, similar to FSD. That is, molecules are built from atoms, organisms from molecules, templates from organisms, and pages from templates. Atomic Design also implies the use of Public API within modules for isolation.

Applicability to frontend

Atomic Design is relatively common in projects. Atomic Design is more popular among web designers than in development. Web designers often use Atomic Design to create scalable and easily maintainable designs. In development, Atomic Design is often mixed with other architectural methodologies. However, since Atomic Design focuses on UI components and their composition, a problem arises with implementing business logic within the architecture. The problem is that Atomic Design does not provide a clear level of responsibility for business logic, leading to its distribution across various components and levels, complicating maintenance and testing. The business logic becomes blurred, making it difficult to clearly separate responsibilities and rendering the code less modular and reusable.

How does it relate to FSD?

In the context of FSD, some elements of Atomic Design can be applied to create flexible and scalable UI components. The atoms and molecules layers can be implemented in shared/ui in FSD, simplifying the reuse and maintenance of basic UI elements.

β”œβ”€β”€ sharedβ”‚  β”œβ”€β”€ uiβ”‚  β”‚  β”œβ”€β”€ atomsβ”‚  β”‚  β”œβ”€β”€ moleculesβ”‚  ...

A comparison of FSD and Atomic Design shows that both methodologies strive for modularity and reusability but focus on different aspects. Atomic Design is oriented towards visual components and their composition. FSD focuses on dividing the application's functionality into independent modules and their interconnections.

Feature Driven

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

About the approach; About applicability in the frontend; FSD position About compatibility, historical development and comparison Feedback

Layers | Feature-Sliced Design

Source: https://feature-sliced.design/docs/reference/layers

On this page Layers are the first level of organisational hierarchy in Feature-Sliced Design. Their purpose is to separate code based on how much responsibility it needs and how many other modules in the app it depends on. Every layer carries special semantic meaning to help you determine how much responsibility you should allocate to your code. There are 7 layers in total, arranged from most responsibility and dependency to least:

  1. App
  2. Processes (deprecated)
  3. Pages
  4. Widgets
  5. Features
  6. Entities
  7. Shared

You don't have to use every layer in your project β€” only add them if you think it brings value to your project. Typically, most frontend projects will have at least the Shared, Pages, and App layers. In practice, layers are folders with lowercase names (for example, πŸ“ shared, πŸ“ pages, πŸ“ app). Adding new layers is not recommended because their semantics are standardized.

Import rule on layers

Layers are made up of slices β€” highly cohesive groups of modules. Dependencies between slices are regulated by the import rule on layers:

A module (file) in a slice can only import other slices when they are located on layers strictly below. For example, the folder πŸ“ ~/features/aaa is a slice with the name "aaa". A file inside of it, ~/features/aaa/api/request.ts, cannot import code from any file in πŸ“ ~/features/bbb, but can import code from πŸ“ ~/entities and πŸ“ ~/shared, as well as any sibling code from πŸ“ ~/features/aaa, for example, ~/features/aaa/lib/cache.ts. Layers App and Shared are exceptions to this rule β€” they are both a layer and a slice at the same time. Slices partition code by business domain, and these two layers are exceptions because Shared does not have business domains, and App combines all business domains. In practice, this means that layers App and Shared are made up of segments, and segments can import each other freely.

Layer definitions

This section describes the semantic meaning of each layer to create an intuition for what kind of code belongs there.

Shared

This layer forms a foundation for the rest of the app. It's a place to create connections with the external world, for example, backends, third-party libraries, the environment. It is also a place to define your own highly contained libraries. This layer, like the App layer, does not contain slices. Slices are intended to divide the layer into business domains, but business domains do not exist in Shared. This means that all files in Shared can reference and import from each other. Here are the segments that you can typically find in this layer: You are free to add more segments, but make sure that the name of these segments describes the purpose of the content, not its essence. For example, components, hooks, and types are bad segment names because they aren't that helpful when you're looking for code.

Entities

Slices on this layer represent concepts from the real world that the project is working with. Commonly, they are the terms that the business uses to describe the product. For example, a social network might work with business entities like User, Post, and Group. An entity slice might contain the data storage (πŸ“ model), data validation schemas (πŸ“ model), entity-related API request functions (πŸ“ api), as well as the visual representation of this entity in the interface (πŸ“ ui). The visual representation doesn't have to produce a complete UI block β€” it is primarily meant to reuse the same appearance across several pages in the app, and different business logic may be attached to it through props or slots.

Entity relationships

Entities in FSD are slices, and by default, slices cannot know about each other. In real life, however, entities often interact with each other, and sometimes one entity owns or contains other entities. Because of that, the business logic of these interactions is preferably kept in higher layers, like Features or Pages. When one entity's data object contains other data objects, usually it's a good idea to make the connection between the entities explicit and side-step the slice isolation by making a cross-reference API with the @x notation. The reason is that connected entities need to be refactored together, so it's best to make the connection impossible to miss. For example: entities/artist/model/artist.ts

import type { Song } from "entities/song/@x/artist";export interface Artist { name: string; songs: Array<Song>;}

entities/song/@x/artist.ts

export type { Song } from "../model/song.ts";

Learn more about the @x notation in the section.

Features

This layer is for the main interactions in your app, things that your users care to do. These interactions often involve business entities, because that's what the app is about. A crucial principle for using the Features layer effectively is: not everything needs to be a feature. A good indicator that something needs to be a feature is the fact that it is reused on several pages. For example, if the app has several editors, and all of them have comments, then comments are a reused feature. Remember that slices are a mechanism for finding code quickly, and if there are too many features, the important ones are drowned out. Ideally, when you arrive in a new project, you would discover its functionality by looking through the pages and features. When deciding on what should be a feature, optimize for the experience of a newcomer to the project to quickly discover large important areas of code. A feature slice might contain the UI to perform the interaction like a form (πŸ“ ui), the API calls needed to make the action (πŸ“ api), validation and internal state (πŸ“ model), feature flags (πŸ“ config).

Widgets

The Widgets layer is intended for large self-sufficient blocks of UI. Widgets are most useful when they are reused across multiple pages, or when the page that they belong to has multiple large independent blocks, and this is one of them. If a block of UI makes up most of the interesting content on a page, and is never reused, it should not be a widget, and instead it should be placed directly inside that page. tip If you're using a nested routing system (like the router of ), it may be helpful to use the Widgets layer in the same way as a flat routing system would use the Pages layer β€” to create full router blocks, complete with related data fetching, loading states, and error boundaries. In the same way, you can store page layouts on this layer.

Pages

Pages are what makes up websites and applications (also known as screens or activities). One page usually corresponds to one slice, however, if there are several very similar pages, they can be grouped into one slice, for example, registration and login forms. There's no limit to how much code you can place in a page slice as long as your team still finds it easy to navigate. If a UI block on a page is not reused, it's perfectly fine to keep it inside the page slice. In a page slice you can typically find the page's UI as well as loading states and error boundaries (πŸ“ ui) and the data fetching and mutating requests (πŸ“ api). It's not common for a page to have a dedicated data model, and tiny bits of state can be kept in the components themselves.

Processes

caution This layer has been deprecated. The current version of the spec recommends avoiding it and moving its contents to features and app instead. Processes are escape hatches for multi-page interactions. This layer is deliberately left undefined. Most applications should not use this layer, and keep router-level and server-level logic on the App layer. Consider using this layer only when the App layer grows large enough to become unmaintainable and needs unloading.

App

All kinds of app-wide matters, both in the technical sense (e.g., context providers) and in the business sense (e.g., analytics). This layer usually doesn't contain slices, as well as Shared, instead having segments directly. Here are the segments that you can typically find in this layer: Feedback

Slices and segments | Feature-Sliced Design

Source: https://feature-sliced.design/docs/reference/slices-segments

On this page

Slices

Slices are the second level in the organizational hierarchy of Feature-Sliced Design. Their main purpose is to group code by its meaning for the product, business, or just the application. The names of slices are not standardized because they are directly determined by the business domain of your application. For example, a photo gallery might have slices photo, effects, gallery-page. A social network would require different slices, for example, post, comments, news-feed. The layers Shared and App don't contain slices. That is because Shared should contain no business logic at all, hence has no meaning for the product, and App should contain only code that concerns the entire application, so no splitting is necessary.

Zero coupling and high cohesion

Slices are meant to be independent and highly cohesive groups of code files. The graphic below might help to visualize the tricky concepts of cohesion and coupling: Image inspired by An ideal slice is independent from other slices on its layer (zero coupling) and contains most of the code related to its primary goal (high cohesion). The independence of slices is enforced by the :

A module (file) in a slice can only import other slices when they are located on layers strictly below.

Public API rule on slices

Inside a slice, the code could be organized in any way that you want. That doesn't pose any issues as long as the slice provides a good public API for other slices to use it. This is enforced with the public API rule on slices:

Every slice (and segment on layers that don't have slices) must contain a public API definition. Modules outside of this slice/segment can only reference the public API, not the internal file structure of the slice/segment. Read more about the rationale of public APIs and the best practices on creating one in the .

Slice groups

Closely related slices can be structurally grouped in a folder, but they should exercise the same isolation rules as other slices β€” there should be no code sharing in that folder.

Segments

Segments are the third and final level in the organizational hierarchy, and their purpose is to group code by its technical nature. There a few standardized segment names: See the for examples of what each of these segments might be used for on different layers. You can also create custom segments. The most common places for custom segments are the App layer and the Shared layer, where slices don't make sense. Make sure that the name of these segments describes the purpose of the content, not its essence. For example, components, hooks, and types are bad segment names because they aren't that helpful when you're looking for code. Feedback

Public API | Feature-Sliced Design

Source: https://feature-sliced.design/docs/reference/public-api

On this page A public API is a contract between a group of modules, like a slice, and the code that uses it. It also acts as a gate, only allowing access to certain objects, and only through that public API. In practice, it's usually implemented as an index file with re-exports: pages/auth/index.js

export { LoginPage } from "./ui/LoginPage";export { RegisterPage } from "./ui/RegisterPage";

What makes a good public API?

A good public API makes using and integrating into other code a slice convenient and reliable. It can be achieved by setting these three goals:

  1. The rest of the application must be protected from structural changes to the slice, like a refactoring
  2. Significant changes in the behavior of the slice that break the previous expectations should cause changes in the public API
  3. Only the necessary parts of the slice should be exposed

The last goal has some important practical implications. It may be tempting to create wildcard re-exports of everything, especially in early development of the slice, because any new objects you export from your files are also automatically exported from the slice: Bad practice, features/comments/index.js

// ❌ BAD CODE BELOW, DON'T DO THISexport * from "./ui/Comment"; // πŸ‘Ž don't try this at homeexport * from "./model/comments"; // πŸ’© this is bad practice

This hurts the discoverability of a slice because you can't easily tell what the interface of this slice is. Not knowing the interface means that you have to dig deep into the code of a slice to understand how to integrate it. Another problem is that you might accidentally expose the module internals, which will make refactoring difficult if someone starts depending on them.

Public API for cross-imports

Cross-imports are a situation when one slice imports from another slice on the same layer. Usually that is prohibited by the , but often there are legitimate reasons to cross-import. For example, business entities often reference each other in the real world, and it's best to reflect these relationships in the code instead of working around them. For this purpose, there's a special kind of public API, also known as the @x-notation. If you have entities A and B, and entity B needs to import from entity A, then entity A can declare a separate public API just for entity B. Then the code inside entities/B/ can import from entities/A/@x/B:

import type { EntityA } from "entities/A/@x/B";

The notation A/@x/B is meant to be read as "A crossed with B". note Try to keep cross-imports to a minimum, and only use this notation on the Entities layer, where eliminating cross-imports is often unreasonable.

Issues with index files

Index files like index.js, also known as barrel files, are the most common way to define a public API. They are easy to make, but they are known to cause problems with certain bundlers and frameworks.

Circular imports

Circular import is when two or more files import each other in a circle. Pictured above: three files, fileA.js, fileB.js, and fileC.js, importing each other in a circle. These situations are often difficult for bundlers to deal with, and in some cases they might even lead to runtime errors that might be difficult to debug. Circular imports can occur without index files, but having an index file presents a clear opporutnity to accidentally create a circular import. It often happens when you have two objects exposed in the public API of a slice, for example, HomePage and loadUserStatistics, and the HomePage needs to access loadUserStatistics, but it does it like this: pages/home/ui/HomePage.jsx

import { loadUserStatistics } from "../"; // importing from pages/home/index.jsexport function HomePage() { /* … */ }

pages/home/index.js

export { HomePage } from "./ui/HomePage";export { loadUserStatistics } from "./api/loadUserStatistics";

This situation creates a circular import, because index.js imports ui/HomePage.jsx, but ui/HomePage.jsx imports index.js. To prevent this issue, consider these two principles. If you have two files, and one imports from the other:

  • When they are in the same slice, always use relative imports and write the full import path
  • When they are in different slices, always use absolute imports, for example, with an alias

Large bundles and broken tree-shaking in Shared

Some bundlers might have a hard time tree-shaking (removing code that isn't imported) when you have an index file that re-exports everything. Usually this isn't a problem for public APIs, because the contents of a module are usually quite closely related, so you would rarely need to import one thing and tree-shake away the other. However, there are two very common cases when the normal rules of public API in FSD may lead to issues β€” shared/ui and shared/lib. These two folders are both collections of unrelated things that often aren't all needed in one place. For example, shared/ui might have modules for every component in the UI library: This problem is made worse when one of these modules has a heavy dependency, like a syntax highlighter or a drag'n'drop library. You don't want to pull those into every page that uses something from shared/ui, for example, a button. If your bundles grow undesirably due to a single public API in shared/ui or shared/lib, it's recommended to instead have a separate index file for each component or library: Then the consumers of these components can import them directly like this: pages/sign-in/ui/SignInPage.jsx

import { Button } from '@/shared/ui/button';import { TextField } from '@/shared/ui/text-field';

No real protection against side-stepping the public API

When you create an index file for a slice, you don't actually forbid anyone from not using it and importing directly. This is especially a problem for auto-imports, because there are several places from which an object can be imported, so the IDE has to decide that for you. Sometimes it might choose to import directly, breaking the public API rule on slices. To catch these issues automatically, we recommend using , an architectural linter with a ruleset for Feature-Sliced Design.

Worse performance of bundlers on large projects

Having a large amount of index files in a project can slow down the development server, as noted by TkDodo in . There are several things you can do to tackle this issue:

  1. The same advice as in β€” have separate index files for each component/library in shared/ui and shared/lib instead of one big one
  2. Avoid having index files in segments on layers that have slices. For example, if you have an index for the feature "comments", πŸ“„ features/comments/index.js, there's no reason to have another index for the ui segment of that feature, πŸ“„ features/comments/ui/index.js.
  3. If you have a very big project, there's a good chance that your application can be split into several big chunks. For example, Google Docs has very different responsibilities for the document editor and for the file browser. You can create a monorepo setup where each package is a separate FSD root, with its own set of layers. Some packages can only have the Shared and Entities layers, others might only have Pages and App, others still might include their own small Shared, but still use the big one from another package too.

Feedback

Motivation | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/motivation

On this page The main idea of Feature-Sliced Design is to facilitate and reduce the cost of developing complex and developing projects, based on . Obviously, this will not be a silver bullet, and of course, the methodology will have its own . Nevertheless, there are reasonable questions regarding the feasibility of such a methodology as a whole note More details

Why are there not enough existing solutions?

It usually, these arguments:

Principles alone are not enough

The existence of principles alone is not enough to design a good architecture Not everyone knows them completely, even fewer understand and apply them correctly The design principles are too general, and do not give a specific answer to the question: "How to design the structure and architecture of a scalable and flexible application?"

Processes don't always work

Documentation/Tests/Processes are, of course, good, but alas, even at high costs for them - they do not always solve the problems posed by the architecture and the introduction of new people into the project

  • The time of entry of each developer into the project is not greatly reduced, because the documentation will most often come out huge / outdated
  • Constantly make sure that everyone understands architecture in the same way-it also requires a huge amount of resources
  • Do not forget about the bus-factor

Existing frameworks cannot be applied everywhere

  • Existing solutions usually have a high entry threshold, which makes it difficult to find new developers
  • Also, most often, the choice of technology has already been determined before the onset of serious problems in the project, and therefore you need to be able to "work with what is" - without being tied to the technology

Q: "In my project React/Vue/Redux/Effector/Mobx/{YOUR_TECH} - how can I better build the structure of entities and the relationships between them?"

As a result

We get "unique as snowflakes" projects, each of which requires a long immersion of the employee, and knowledge that is unlikely to be applicable on another project

@sergeysova: "This is exactly the situation that currently exists in our field of frontend development: each lead will invent different architectures and project structures, while it is not a fact that these structures will pass the test of time, as a result, a maximum of two people can develop the project besides him, and each new developer needs to be immersed again."

Why do developers need the methodology?

Focus on business features, not on architecture problems

The methodology allows you to save resources on designing a scalable and flexible architecture, instead directing the attention of developers to the development of the main functionality. At the same time, the architectural solutions themselves are standardized from project to project. A separate question is that the methodology should earn the trust of the community, so that another developer can get acquainted with it and rely on it in solving the problems of his project within the time available to him

An experience-proven solution

The methodology is designed for developers who are aimed at a proven solution for designing complex business logic However, it is clear that the methodology is generally about a set of best-practices, articles that address certain problems and cases during development. Therefore, the methodology will also be useful for the rest of the developers-who somehow face problems during development and design

Project Health

The methodology will allow to solve and track the problems of the project in advance, without requiring a huge amount of resources Most often, technical debt accumulates and accumulates over time, and the responsibility for its resolution lies on both the lead and the team The methodology will allow you to warn possible problems in the scaling and development of the project in advance

Why does a business need a methodology?

Fast onboarding

With the methodology, you can hire a person to the project who is already previously familiar with this approach, and not train again People start to understand and benefit the project faster, and there are additional guarantees to find people for the next iterations of the project

An experience-proven solution

With the methodology, the business will get a solution for most of the issues that arise during the development of systems Since most often a business wants to get a framework / solution that would solve the lion's share of problems during the development of the project

Applicability for different stages of the project

The methodology can benefit the project both at the stage of project support and development, and at the MVP stage Yes, the most important thing for MVP is "features, not the architecture laid down for the future". But even in conditions of limited deadlines, knowing the best-practices from the methodology, you can "do with little blood", when designing the MVP version of the system, finding a reasonable compromise (rather than modeling features "at random") The same can be said about testing

When is our methodology not needed?

Business Size

  • Small business - most often needs a ready-made and very fast solution. Only when the business grows (at least to almost average), he understands that in order for customers to continue using, it is necessary, among other things, to devote time to the quality and stability of the solutions being developed
  • Medium-sized business - usually understands all the problems of development, and even if it is necessary to "arrange a race for features", he still spends time on quality improvements, refactoring and tests (and of course-on an extensible architecture)
  • Big business - usually already has an extensive audience, staff, and a much more extensive set of its practices, and probably even its own approach to architecture, so the idea of taking someone else's comes to them not so often

Plans

The main part of the goals , but in addition, it is worth talking about our expectations from the methodology in the future

Combining experience

Now we are trying to combine all our diverse experience of the core-team, and get a methodology hardened by practice as a result Of course, we can get Angular 3.0 as a result, but it is much more important here to investigate the very problem of designing the architecture of complex systems And yes - we have complaints about the current version of the methodology, but we want to work together to come to a single and optimal solution (taking into account, among other things, the experience of the community)

Life outside the specification

If everything goes well, then the methodology will not be limited only to the specification and the toolkit

See also

Feedback

Integration aspects | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/promote/integration

On this page

Summary

First 5 minutes (RU):

Also

Advantages:

  • CodeReview
  • Onboarding

Disadvantages: Feedback

Mission | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/mission

On this page Here we describe the goals and limitations of the applicability of the methodology-which we are guided by when developing the methodology

  • We see our goal as a balance between ideology and simplicity
  • We won't be able to make a silver bullet that fits everyone

Nevertheless, the methodology should be close and accessible to a fairly wide range of developers

Goals

Intuitive clarity for a wide range of developers

The methodology should be accessible - for most of the team in projects Because even with all the future tools , it will not be enough, if only experienced seniors/leads will understand the methodology

Solving everyday problems

The methodology should set out the reasons and solutions to our everyday problems when developing projects And also-attach tools to all this (cli, linters) So that developers can use a battle-tested approach that allows them to bypass long-standing problems of architecture and development

@sergeysova: Imagine, that a developer writes code within the framework of the methodology and he has problems 10 times less often, simply because other people have thought out the solution to many problems.

Limitations

We do not want to impose our point of view, and at the same time we understand that many of our habits, as developers, interfere from day to day Everyone has their own level of experience in designing and developing systems, therefore, it is worth understanding the following:

See also

Feedback

FAQ | Feature-Sliced Design

Source: https://feature-sliced.design/docs/get-started/faq

On this page info You can ask your question in our , , and .

Is there a toolkit or a linter?

Yes! We have a linter called to check your project's architecture and through a CLI or IDEs.

Where to store the layout/template of pages?

If you need plain markup layouts, you can keep them in shared/ui. If you need to use higher layers inside, there are a few options:

  • Perhaps you don't need layouts at all? If the layout is only a few lines, it might be reasonable to duplicate the code in each page rather than try to abstract it.
  • If you do need layouts, you can have them as separate widgets or pages, and compose them in your router configuration in App. Nested routing is another option.

What is the difference between a feature and an entity?

An entity is a real-life concept that your app is working with. A feature is an interaction that provides real-life value to your app’s users, the thing people want to do with your entities. For more information, along with examples, see the Reference page on .

Can I embed pages/features/entities into each other?

Yes, but this embedding should happen in higher layers. For example, inside a widget, you can import both features and then insert one feature into another as props/children. You cannot import one feature from another feature, this is prohibited by the .

What about Atomic Design?

The current version of the methodology does not require nor prohibit the use of Atomic Design together with Feature-Sliced Design. For example, Atomic Design for the ui segment of modules.

Are there any useful resources/articles/etc. about FSD?

Yes!

Why do I need Feature-Sliced Design?

It helps you and your team to quickly overview the project in terms of its main value-bringing components. A standardized architecture helps to speed up onboarding and resolves debates about code structure. See the page to learn more about why FSD was created.

Does a novice developer need an architecture/methodology?

Rather yes than no Usually, when you design and develop a project in one person, everything goes smoothly. But if there are pauses in development, new developers are added to the team - then problems come

How do I work with the authorization context?

Answered Feedback

About architecture | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/architecture

On this page

Problems

Usually, the conversation about architecture is raised when the development stops due to certain problems in the project.

Bus-factor & Onboarding

Only a limited number of people understand the project and its architecture Examples:

  • "It's difficult to add a person to the development"
  • "For every problem, everyone has their own opinion on how to get around" (let's envy the angular)
  • "I don't understand what is happening in this big piece of monolith"

Implicit and uncontrolled consequences

A lot of implicit side effects during development/refactoring ("everything depends on everything") Examples:

  • "The feature imports the feature"
  • "I updated the store of one page, and the functionality fell off on the other"
  • "The logic is smeared all over the application, and it is impossible to track where the beginning is, where the end is"

Uncontrolled reuse of logic

It is difficult to reuse/modify existing logic At the same time, there are usually :

  • Either the logic is written completely from scratch for each module (with possible repetitions in the existing codebase)
  • Either there is a tendency to transfer all-all implemented modules to shared folders, thereby creating a large dump of modules from it (where most are used only in one place)

Examples:

  • "I have N implementations of the same business logic in my project, for which I still pay"
  • "There are 6 different components of the button/pop-up/... In the project"
  • "Dump of helpers"

Requirements

Therefore, it seems logical to present the desired requirements for an ideal architecture: note Wherever it says "easy", it means "relatively easy for a wide range of developers", because it is clear that

Explicitness

Control

Adaptability

See also

Feedback

Promote in company | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/promote/for-company

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

Do the project and the company need a methodology?

About the justification of the application, Those duty

How can I submit a methodology to a business?

How to prepare and justify a plan to move to the methodology?

Feedback

Knowledge types in the project | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/knowledge-types

On this page The following "types of knowledge" can be distinguished in any project:

  • Fundamental knowledge Knowledge that does not change much over time, such as algorithms, computer science, programming language mechanisms and its APIs.
  • Technology stack Knowledge of the set of technical solutions used in a project, including programming languages, frameworks, and libraries.
  • Project knowledge Knowledge that is specific to the current project and not valuable outside of it. This knowledge is essential for newly-onboarded developers to be able to contribute effectively.

note Feature-Sliced Design is designed to reduce reliance on "project knowledge", take more responsibility, and make it easier to onboard new team members.

See also

Feedback

Abstractions | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/abstractions

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

The law of leaky abstractions

Why are there so many abstractions

Abstractions help to cope with the complexity of the project. The question is - will these abstractions be specific only for this project, or will we try to derive general abstractions based on the specifics of the frontend Architecture and applications in general are inherently complex, and the only question is how to better distribute and describe this complexity

About scopes of responsibility

About optional abstractions

See also

Feedback

Naming | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/naming

On this page Different developers have different experiences and contexts, which can lead to misunderstandings on the team when the same entities are called differently. For example:

  • Components for display can be called "ui", "components", "ui-kit", "views", …
  • The code that is reused throughout the application can be called "core", "shared", "app", …
  • Business logic code can be called "store", "model", "state", …

Naming in Feature-Sliced Design

The methodology uses specific terms such as:

  • "app", "process", "page", "feature", "entity", "shared" as layer names,
  • "ui', "model", "lib", "api", "config" as segment names.

It is very important to stick to these terms to prevent confusion among team members and new developers joining the project. Using standard names also helps when asking for help from the community.

Naming Conflicts

Naming conflicts can occur when terms used in the FSD methodology overlap with terms used in the business:

  • FSD#process vs simulated process in an application,
  • FSD#page vs log page,
  • FSD#model vs car model.

For example, a developer who sees the word "process" in the code will spend extra time trying to figure out what process is meant. Such collisions can disrupt the development process. When the project glossary contains terminology specific to FSD, it is critical to be careful when discussing these terms with the team and technical disinterested parties. To communicate effectively with the team, it is recommended that the abbreviation "FSD" be used to prefix the methodology terms. For example, when talking about a process, you might say, "We can put this process on the FSD features layer." Conversely, when communicating with non-technical stakeholders, it is better to limit the use of FSD terminology and refrain from mentioning the internal structure of the code base.

See also

Feedback

Signals of architecture | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/signals

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

If there is a limitation on the part of the architecture, then there are obvious reasons for this, and consequences if they are ignored The methodology and architecture gives signals, and how to deal with it depends on what risks you are ready to take on and what is most suitable for your team)

See also

Feedback

Partial Application | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/promote/partial-application

WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

How to partially apply the methodology? Does it make sense? What if I ignore it? Feedback

Needs driven | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/understanding/needs-driven

On this page TL;DR β€” Can't you formulate the goal that the new feature will solve? Or maybe the problem is that the task itself is not formulated? The point is also that the methodology helps to pull out the problematic definition of tasks and goals β€” project does not live in static - requirements and functionality are constantly changing. Over time, the code turns into mush, because at the start the project was designed only for the initial impression of wishes. And the task of a good architecture is also to be sharpened for changing development conditions.

Why?

To choose a clear name for an entity and understand its components, you need to clearly understand what task will be solved with the help of all this code.

@sergeysova: During development, we try to give each entity or function a name that clearly reflects the intentions and meaning of the code being executed. After all, without understanding the task, it is impossible to write the right tests that cover the most important cases, put down errors that help the user in the right places, even it is banal not to interrupt the user's flow because of fixable non-critical errors.

What tasks are we talking about?

Frontend develops applications and interfaces for end users, so we solve the tasks of these consumers. When a person comes to us, he wants to solve some of his pain or close a need. The task of managers and analysts is to formulate this need, and implement developers taking into account the features of web development (loss of communication, backend error, typo, missed the cursor or finger). This very goal, with which the user came, is the task of the developers.

One small solved problem is a feature in the Feature-Sliced Design methodology β€” you need to cut the entire scope of project tasks into small goals.

How does this affect development?

Task decomposition

When a developer begins to implement a task, in order to simplify the understanding and support of the code, he mentally cuts it into stages:

  • first split into top-level entities and implement them,
  • then these entities split into smaller ones
  • and so on

In the process of splitting into entities, the developer is forced to give them a name that would clearly reflect his idea and help to understand what task the code solves when reading the listing At the same time, we do not forget that we are trying to help the user reduce pain or realize needs

Understanding the essence of the task

But to give a clear name to an entity, the developer must know enough about its purpose It is not difficult to draw a conclusion: while the developer will reflect on the name of entities within the framework of the methodology, he will be able to find poorly formulated tasks even before writing the code.

How to give a name to an entity if you do not understand well what tasks it can solve, how can you even divide a task into entities if you do not understand it well?

How to formulate it?

To formulate a task that is solved by features, you need to understand the task itself, and this is already the responsibility of the project manager and analysts. The methodology can only tell the developer what tasks the product manager should pay close attention to.

@sergeysova: the Whole frontend is primarily a display of information, any component in the first turn, displays, and then the task "to show the user something" has no practical value. Even without taking into account the specifics of the frontend can ask, "why do I have to show you", so you can continue to ask until't get out of pain or the need of the consumer. As soon as we were able to get to the basic needs or pains, we can go back and figure out how exactly your product or service can help the user with his goals Any new task in your tracker is aimed at solving business problems, and the business tries to solve the user's tasks at the same time earning money on it. This means that each task has certain goals, even if they are not spelled out in the description text. The developer must clearly understand what goal this or that task is pursuing, but not every company can afford to build processes perfectly, although this is a separate conversation, nevertheless, the developer may well "ping" the right managers himself to find out this and do his part of the work effectively.

And what is the benefit?

Now let's look at the whole process from beginning to end.

1. Understanding user tasks

When a developer understands his pain and how the business closes them, he can offer solutions that are not available to the business due to the specifics of web development.

But of course, all this can work only if the developer is not indifferent to what he is doing and for what, otherwise why then the methodology and some approaches?

2. Structuring and ordering

With the understanding of tasks comes a clear structure both in the head and in the tasks along with the code

3. Understanding the feature and its components

One feature is one useful functionality for the user

@sergeysova: The point is that the feature contains only code that implements the functionality itself, without unnecessary details and internal solutions (ideally)* Open the feature code and see only what relates to the task - no more

4. Profit

Business very rarely turns its course radically in the other direction, which means the reflection of business tasks in the frontend application code is a very significant profit. Then you don't have to explain to each new team member what this or that code does, and in general why it was added - everything will be explained through the business tasks that are already reflected in the code.

What is called

Back to reality

If business processes are understood and good names are given at the design stage - then it is not particularly problematic to transfer this understanding and logic to the code. However, in practice, tasks and functionality are usually developed "too" iteratively and (or) there is no time to think through the design. As a result, the feature makes sense today, and if you expand this feature in a month, you can rewrite the gender of the project.

[From the discussion]: The developer tries to think 2-3 steps ahead, taking into account future wishes, but here he rests on his own experience Burns experience engineer usually immediately looking 10 steps ahead, and understand where one feature to divide and combine with the other But sometimes that comes the task which had to face the experience, and nowhere to take the understanding of how literacy to decompose, with the least unfortunate consequences in the future

The role of methodology

The methodology helps to solve the problems of developers, so that it is easier to solve the problems of users. There is no solution to the problems of developers only for the sake of developers But in order for the developer to solve his tasks, you need to understand the user's tasks - on the contrary, it will not work

Methodology requirements

It becomes clear that you need to identify at least two requirements for Feature-Sliced Design:

  1. The methodology should tell how to create features, processes and entities
    • Which means it should clearly explain how to divide the code between them, which means that the naming of these entities should also be laid down in the specification.
  2. The methodology should help the architecture easily adapt to the changing requirements of the project

See also

  • The current article is an adaptation of this discussion, you can read the full uncut version at the link

Feedback

Promote in team | Feature-Sliced Design

Source: https://feature-sliced.design/docs/about/promote/for-team

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

  • Onboard newcomers
  • Development Guidelines ("where to search N module", etc...)
  • New approach for tasks

See also

Feedback

Tutorial | Feature-Sliced Design

Source: https://feature-sliced.design/docs/get-started/tutorial

On this page

Part 1. On paper

This tutorial will examine the Real World App, also known as Conduit. Conduit is a basic clone β€” it lets you read and write articles as well as comment on the articles of others. This is a pretty small application, so we will keep it simple and avoid excessive decomposition. It’s highly likely that the entire app will fit into just three layers: App, Pages, and Shared. If not, we will introduce additional layers as we go. Ready?

Start by listing the pages

If we look at the screenshot above, we can assume at least the following pages: Every one of these pages will become its own slice on the Pages layer. Recall from the overview that slices are simply folders inside of layers and layers are simply folders with predefined names like pages. As such, our Pages folder will look like this:

πŸ“‚ pages/ πŸ“ feed/ πŸ“ sign-in/ πŸ“ article-read/ πŸ“ article-edit/ πŸ“ profile/ πŸ“ settings/

The key difference of Feature-Sliced Design from an unregulated code structure is that pages cannot reference each other. That is, one page cannot import code from another page. This is due to the import rule on layers: A module (file) in a slice can only import other slices when they are located on layers strictly below. In this case, a page is a slice, so modules (files) inside this page can only reference code from layers below, not from the same layer, Pages.

Close look at the feed

Anonymous user’s perspective Authenticated user’s perspective There are three dynamic areas on the feed page:

  1. Sign-in links with an indication if you are signed in
  2. List of tags that triggers filtering in the feed
  3. One/two feeds of articles, each article with a like button

The sign-in links are a part of a header that is common to all pages, we will revisit it separately.

List of tags

To build the list of tags, we need to fetch the available tags, render each tag as a chip, and store the selected tags in a client-side storage. These operations fall into categories β€œAPI interaction”, β€œuser interface”, and β€œstorage”, respectively. In Feature-Sliced Design, code is separated by purpose using segments. Segments are folders in slices, and they can have arbitrary names that describe the purpose, but some purposes are so common that there’s a convention for certain segment names: We will place code that fetches tags into api, the tag component into ui, and the storage interaction into model.

Articles

Using the same grouping principles, we can decompose the feed of articles into the same three segments:

Reuse generic code

Most pages are very different in intent, but certain things stay the same across the entire app β€” for example, the UI kit that conforms to the design language, or the convention on the backend that everything is done with a REST API with the same authentication method. Since slices are meant to be isolated, code reuse is facilitated by a lower layer, Shared. Shared is different from other layers in the sense that it contains segments, not slices. In this way, the Shared layer can be thought of as a hybrid between a layer and a slice. Usually, the code in Shared is not planned ahead of time, but rather extracted during development, because only during development does it become clear which parts of code are actually shared. However, it’s still helpful to keep a mental note of what kind of code naturally belongs in Shared: Those are just a few examples of segment names in Shared, but you can omit any of them or create your own. The only important thing to remember when creating new segments is that segment names should describe purpose (the why), not essence (the what). Names like β€œcomponents”, β€œhooks”, β€œmodals” should not be used because they describe what these files are, but don’t help to navigate the code inside. This requires people on the team to dig through every file in such folders and also keeps unrelated code close, which leads to broad areas of code being affected by refactoring and thus makes code review and testing harder.

Define a strict public API

In the context of Feature-Sliced Design, the term public API refers to a slice or segment declaring what can be imported from it by other modules in the project. For example, in JavaScript that can be an index.js file re-exporting objects from other files in the slice. This enables freedom in refactoring code inside a slice as long as the contract with the outside world (i.e. the public API) stays the same. For the Shared layer that has no slices, it’s usually more convenient to define a separate public API for each segment as opposed to defining one single index of everything in Shared. This keeps imports from Shared naturally organized by intent. For other layers that have slices, the opposite is true β€” it’s usually more practical to define one index per slice and let the slice decide its own set of segments that is unknown to the outside world because other layers usually have a lot less exports. Our slices/segments will appear to each other as follows:

πŸ“‚ pages/ πŸ“‚ feed/  πŸ“„ index πŸ“‚ sign-in/  πŸ“„ index πŸ“‚ article-read/  πŸ“„ index πŸ“ β€¦πŸ“‚ shared/ πŸ“‚ ui/  πŸ“„ index πŸ“‚ api/  πŸ“„ index πŸ“ …

Whatever is inside folders like pages/feed or shared/ui is only known to those folders, and other files should not rely on the internal structure of these folders.

Large reused blocks in the UI

Earlier we made a note to revisit the header that appears on every page. Rebuilding it from scratch on every page would be impractical, so it’s only natural to want to reuse it. We already have Shared to facilitate code reuse, however, there’s a caveat to putting large blocks of UI in Shared β€” the Shared layer is not supposed to know about any of the layers above. Between Shared and Pages there are three other layers: Entities, Features, and Widgets. Some projects may have something in those layers that they need in a large reusable block, and that means we can’t put that reusable block in Shared, or else it would be importing from upper layers, which is prohibited. That’s where the Widgets layer comes in. It is located above Shared, Entities, and Features, so it can use them all. In our case, the header is very simple β€” it’s a static logo and top-level navigation. The navigation needs to make a request to the API to determine if the user is currently logged in or not, but that can be handled by a simple import from the api segment. Therefore, we will keep our header in Shared.

Close look at a page with a form

Let’s also examine a page that’s intended for editing, not reading. For example, the article writer: It looks trivial, but contains several aspects of application development that we haven’t explored yet β€” form validation, error states, and data persistence. If we were to build this page, we would grab some inputs and buttons from Shared and put together a form in the ui segment of this page. Then, in the api segment, we would define a mutation request to create the article on the backend. To validate the request before sending, we need a validation schema, and a good place for it is the model segment, since it’s the data model. There we will produce error messages and display them using another component in the ui segment. To improve user experience, we could also persist the inputs to prevent accidental data loss. This is also a job of the model segment.

Summary

We have examined several pages and outlined a preliminary structure for our application:

  1. Shared layer 1. ui will contain our reusable UI kit 2. api will contain our primitive interactions with the backend 3. The rest will be arranged on demand
  2. Pages layer β€” each page is a separate slice 1. ui will contain the page itself and all of its parts 2. api will contain more specialized data fetching, using shared/api 3. model might contain client-side storage of the data that we will display

Let’s get building!

Part 2. In code

Now that we have a plan, let’s put it to practice. We will use React and . There's a template ready for this project, clone it from GitHub to get a headstart: . Install dependencies with npm install and start the development server with npm run dev. Open and you should see a blank app.

Lay out the pages

Let’s start by creating blank components for all our pages. Run the following command in your project:

npx fsd pages feed sign-in article-read article-edit profile settings --segments ui

This will create folders like pages/feed/ui/ and an index file, pages/feed/index.ts, for every page.

Connect the feed page

Let’s connect the root route of our application to the feed page. Create a component, FeedPage.tsx in pages/feed/ui and put the following inside it: pages/feed/ui/FeedPage.tsx

export function FeedPage() { return (  <div className="home-page">   <div className="banner">    <div className="container">     <h1 className="logo-font">conduit</h1>     <p>A place to share your knowledge.</p>    </div>   </div>  </div> );}

Then re-export this component in the feed page’s public API, the pages/feed/index.ts file: pages/feed/index.ts

export { FeedPage } from "./ui/FeedPage";

Now connect it to the root route. In Remix, routing is file-based, and the route files are located in the app/routes folder, which nicely coincides with Feature-Sliced Design. Use the FeedPage component in app/routes/_index.tsx: app/routes/_index.tsx

import type { MetaFunction } from "@remix-run/node";import { FeedPage } from "pages/feed";export const meta: MetaFunction = () => { return [{ title: "Conduit" }];};export default FeedPage;

Then, if you run the dev server and open the application, you should see the Conduit banner!

API client

To talk to the RealWorld backend, let’s create a convenient API client in Shared. Create two segments, api for the client and config for variables like the backend base URL:

npx fsd shared --segments api config

Then create shared/config/backend.ts: shared/config/backend.ts

export const backendBaseUrl = "https://api.realworld.io/api";

shared/config/index.ts

export { backendBaseUrl } from "./backend";

Since the RealWorld project conveniently provides an , we can take advantage of auto-generated types for our client. We will use that comes with an additional type generator. Run the following command to generate up-to-date API typings:

npm run generate-api-types

This will create a file shared/api/v1.d.ts. We will use this file to create a typed API client in shared/api/client.ts: shared/api/client.ts

import createClient from "openapi-fetch";import { backendBaseUrl } from "shared/config";import type { paths } from "./v1";export const { GET, POST, PUT, DELETE } = createClient<paths>({ baseUrl: backendBaseUrl });

shared/api/index.ts

export { GET, POST, PUT, DELETE } from "./client";

Real data in the feed

We can now proceed to adding articles to the feed, fetched from the backend. Let’s begin by implementing an article preview component. Create pages/feed/ui/ArticlePreview.tsx with the following content: pages/feed/ui/ArticlePreview.tsx

export function ArticlePreview({ article }) { /* TODO */ }

Since we’re writing in TypeScript, it would be nice to have a typed article object. If we explore the generated v1.d.ts, we can see that the article object is available through components["schemas"]["Article"]. So let’s create a file with our data models in Shared and export the models: shared/api/models.ts

import type { components } from "./v1";export type Article = components["schemas"]["Article"];

shared/api/index.ts

export { GET, POST, PUT, DELETE } from "./client";export type { Article } from "./models";

Now we can come back to the article preview component and fill the markup with data. Update the component with the following content: pages/feed/ui/ArticlePreview.tsx

import { Link } from "@remix-run/react";import type { Article } from "shared/api";interface ArticlePreviewProps { article: Article;}export function ArticlePreview({ article }: ArticlePreviewProps) { return (  <div className="article-preview">   <div className="article-meta">    <Link to={`/profile/${article.author.username}`} prefetch="intent">     <img src={article.author.image} alt="" />    </Link>    <div className="info">     <Link      to={`/profile/${article.author.username}`}      className="author"      prefetch="intent"     >      {article.author.username}     </Link>     <span className="date" suppressHydrationWarning>      {new Date(article.createdAt).toLocaleDateString(undefined, {       dateStyle: "long",      })}     </span>    </div>    <button className="btn btn-outline-primary btn-sm pull-xs-right">     <i className="ion-heart"></i> {article.favoritesCount}    </button>   </div>   <Link    to={`/article/${article.slug}`}    className="preview-link"    prefetch="intent"   >    <h1>{article.title}</h1>    <p>{article.description}</p>    <span>Read more...</span>    <ul className="tag-list">     {article.tagList.map((tag) => (      <li key={tag} className="tag-default tag-pill tag-outline">       {tag}      </li>     ))}    </ul>   </Link>  </div> );}

The like button doesn’t do anything for now, we will fix that when we get to the article reader page and implement the liking functionality. Now we can fetch the articles and render out a bunch of these cards. Fetching data in Remix is done with loaders β€” server-side functions that fetch exactly what a page needs. Loaders interact with the API on the page’s behalf, so we will put them in the api segment of a page: pages/feed/api/loader.ts

import { json } from "@remix-run/node";import { GET } from "shared/api";export const loader = async () => { const { data: articles, error, response } = await GET("/articles"); if (error !== undefined) {  throw json(error, { status: response.status }); } return json({ articles });};

To connect it to the page, we need to export it with the name loader from the route file: pages/feed/index.ts

export { FeedPage } from "./ui/FeedPage";export { loader } from "./api/loader";

app/routes/_index.tsx

import type { MetaFunction } from "@remix-run/node";import { FeedPage } from "pages/feed";export { loader } from "pages/feed";export const meta: MetaFunction = () => { return [{ title: "Conduit" }];};export default FeedPage;

And the final step is to render these cards in the feed. Update your FeedPage with the following code: pages/feed/ui/FeedPage.tsx

import { useLoaderData } from "@remix-run/react";import type { loader } from "../api/loader";import { ArticlePreview } from "./ArticlePreview";export function FeedPage() { const { articles } = useLoaderData<typeof loader>(); return (  <div className="home-page">   <div className="banner">    <div className="container">     <h1 className="logo-font">conduit</h1>     <p>A place to share your knowledge.</p>    </div>   </div>   <div className="container page">    <div className="row">     <div className="col-md-9">      {articles.articles.map((article) => (       <ArticlePreview key={article.slug} article={article} />      ))}     </div>    </div>   </div>  </div> );}

Filtering by tag

Regarding the tags, our job is to fetch them from the backend and to store the currently selected tag. We already know how to do fetching β€” it’s another request from the loader. We will use a convenience function promiseHash from a package remix-utils, which is already installed. Update the loader file, pages/feed/api/loader.ts, with the following code: pages/feed/api/loader.ts

import { json } from "@remix-run/node";import type { FetchResponse } from "openapi-fetch";import { promiseHash } from "remix-utils/promise";import { GET } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { const { data, error, response } = await responsePromise; if (error !== undefined) {  throw json(error, { status: response.status }); } return data as NonNullable<typeof data>;}export const loader = async () => { return json(  await promiseHash({   articles: throwAnyErrors(GET("/articles")),   tags: throwAnyErrors(GET("/tags")),  }), );};

You might notice that we extracted the error handling into a generic function throwAnyErrors. It looks pretty useful, so we might want to reuse it later, but for now let’s just keep an eye on it. Now, to the list of tags. It needs to be interactive β€” clicking on a tag should make that tag selected. By Remix convention, we will use the URL search parameters as our storage for the selected tag. Let the browser take care of storage while we focus on more important things. Update pages/feed/ui/FeedPage.tsx with the following code: pages/feed/ui/FeedPage.tsx

import { Form, useLoaderData } from "@remix-run/react";import { ExistingSearchParams } from "remix-utils/existing-search-params";import type { loader } from "../api/loader";import { ArticlePreview } from "./ArticlePreview";export function FeedPage() { const { articles, tags } = useLoaderData<typeof loader>(); return (  <div className="home-page">   <div className="banner">    <div className="container">     <h1 className="logo-font">conduit</h1>     <p>A place to share your knowledge.</p>    </div>   </div>   <div className="container page">    <div className="row">     <div className="col-md-9">      {articles.articles.map((article) => (       <ArticlePreview key={article.slug} article={article} />      ))}     </div>     <div className="col-md-3">      <div className="sidebar">       <p>Popular Tags</p>       <Form>        <ExistingSearchParams exclude={["tag"]} />        <div className="tag-list">         {tags.tags.map((tag) => (          <button           key={tag}           name="tag"           value={tag}           className="tag-pill tag-default"          >           {tag}          </button>         ))}        </div>       </Form>      </div>     </div>    </div>   </div>  </div> );} 

Then we need to use the tag search parameter in our loader. Change the loader function in pages/feed/api/loader.ts to the following: pages/feed/api/loader.ts

import { json, type LoaderFunctionArgs } from "@remix-run/node";import type { FetchResponse } from "openapi-fetch";import { promiseHash } from "remix-utils/promise";import { GET } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { const { data, error, response } = await responsePromise; if (error !== undefined) {  throw json(error, { status: response.status }); } return data as NonNullable<typeof data>;}export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; return json(  await promiseHash({   articles: throwAnyErrors(    GET("/articles", { params: { query: { tag: selectedTag } } }),   ),   tags: throwAnyErrors(GET("/tags")),  }), );};

That’s it, no model segment necessary. Remix is pretty neat.

Pagination

In a similar fashion, we can implement the pagination. Feel free to give it a shot yourself or just copy the code below. There’s no one to judge you anyway. pages/feed/api/loader.ts

import { json, type LoaderFunctionArgs } from "@remix-run/node";import type { FetchResponse } from "openapi-fetch";import { promiseHash } from "remix-utils/promise";import { GET } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { const { data, error, response } = await responsePromise; if (error !== undefined) {  throw json(error, { status: response.status }); } return data as NonNullable<typeof data>;}/** Amount of articles on one page. */export const LIMIT = 20;export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); return json(  await promiseHash({   articles: throwAnyErrors(    GET("/articles", {     params: {      query: {       tag: selectedTag,       limit: LIMIT,       offset: !Number.isNaN(page) ? page * LIMIT : undefined,      },     },    }),   ),   tags: throwAnyErrors(GET("/tags")),  }), );};

pages/feed/ui/FeedPage.tsx

import { Form, useLoaderData, useSearchParams } from "@remix-run/react";import { ExistingSearchParams } from "remix-utils/existing-search-params";import { LIMIT, type loader } from "../api/loader";import { ArticlePreview } from "./ArticlePreview";export function FeedPage() { const [searchParams] = useSearchParams(); const { articles, tags } = useLoaderData<typeof loader>(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (  <div className="home-page">   <div className="banner">    <div className="container">     <h1 className="logo-font">conduit</h1>     <p>A place to share your knowledge.</p>    </div>   </div>   <div className="container page">    <div className="row">     <div className="col-md-9">      {articles.articles.map((article) => (       <ArticlePreview key={article.slug} article={article} />      ))}      <Form>       <ExistingSearchParams exclude={["page"]} />       <ul className="pagination">        {Array(pageAmount)         .fill(null)         .map((_, index) =>          index + 1 === currentPage ? (           <li key={index} className="page-item active">            <span className="page-link">{index + 1}</span>           </li>          ) : (           <li key={index} className="page-item">            <button             className="page-link"             name="page"             value={index + 1}            >             {index + 1}            </button>           </li>          ),         )}       </ul>      </Form>     </div>     <div className="col-md-3">      <div className="sidebar">       <p>Popular Tags</p>       <Form>        <ExistingSearchParams exclude={["tag", "page"]} />        <div className="tag-list">         {tags.tags.map((tag) => (          <button           key={tag}           name="tag"           value={tag}           className="tag-pill tag-default"          >           {tag}          </button>         ))}        </div>       </Form>      </div>     </div>    </div>   </div>  </div> );}

So that’s also done. There’s also the tab list that can be similarly implemented, but let’s hold on to that until we implement authentication. Speaking of which!

Authentication

Authentication involves two pages β€” one to login and another to register. They are mostly the same, so it makes sense to keep them in the same slice, sign-in, so that they can reuse code if needed. Create RegisterPage.tsx in the ui segment of pages/sign-in with the following content: pages/sign-in/ui/RegisterPage.tsx

import { Form, Link, useActionData } from "@remix-run/react";import type { register } from "../api/register";export function RegisterPage() { const registerData = useActionData<typeof register>(); return (  <div className="auth-page">   <div className="container page">    <div className="row">     <div className="col-md-6 offset-md-3 col-xs-12">      <h1 className="text-xs-center">Sign up</h1>      <p className="text-xs-center">       <Link to="/login">Have an account?</Link>      </p>      {registerData?.error && (       <ul className="error-messages">        {registerData.error.errors.body.map((error) => (         <li key={error}>{error}</li>        ))}       </ul>      )}      <Form method="post">       <fieldset className="form-group">        <input         className="form-control form-control-lg"         type="text"         name="username"         placeholder="Username"        />       </fieldset>       <fieldset className="form-group">        <input         className="form-control form-control-lg"         type="text"         name="email"         placeholder="Email"        />       </fieldset>       <fieldset className="form-group">        <input         className="form-control form-control-lg"         type="password"         name="password"         placeholder="Password"        />       </fieldset>       <button className="btn btn-lg btn-primary pull-xs-right">        Sign up       </button>      </Form>     </div>    </div>   </div>  </div> );}

We have a broken import to fix now. It involves a new segment, so create that:

npx fsd pages sign-in -s api

However, before we can implement the backend part of registering, we need some infrastructure code for Remix to handle sessions. That goes to Shared, in case any other page needs it. Put the following code in shared/api/auth.server.ts. This is highly Remix-specific, so don’t worry too much about it, just copy-paste: shared/api/auth.server.ts

import { createCookieSessionStorage, redirect } from "@remix-run/node";import invariant from "tiny-invariant";import type { User } from "./models";invariant( process.env.SESSION_SECRET, "SESSION_SECRET must be set for authentication to work",);const sessionStorage = createCookieSessionStorage<{ user: User;}>({ cookie: {  name: "__session",  httpOnly: true,  path: "/",  sameSite: "lax",  secrets: [process.env.SESSION_SECRET],  secure: process.env.NODE_ENV === "production", },});export async function createUserSession({ request, user, redirectTo,}: { request: Request; user: User; redirectTo: string;}) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); session.set("user", user); return redirect(redirectTo, {  headers: {   "Set-Cookie": await sessionStorage.commitSession(session, {    maxAge: 60 * 60 * 24 * 7, // 7 days   }),  }, });}export async function getUserFromSession(request: Request) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); return session.get("user") ?? null;}export async function requireUser(request: Request) { const user = await getUserFromSession(request); if (user === null) {  throw redirect("/login"); } return user;}

And also export the User model from the models.ts file right next to it: shared/api/models.ts

import type { components } from "./v1";export type Article = components["schemas"]["Article"];export type User = components["schemas"]["User"];

Before this code can work, the SESSION_SECRET environment variable needs to be set. Create a file called .env in the root of the project, write SESSION_SECRET= and then mash some keys on your keyboard to create a long random string. You should get something like this: .env

SESSION_SECRET=dontyoudarecopypastethis

Finally, add some exports to the public API to make use of this code: shared/api/index.ts

export { GET, POST, PUT, DELETE } from "./client";export type { Article } from "./models";export { createUserSession, getUserFromSession, requireUser } from "./auth.server";

Now we can write the code that will talk to the RealWorld backend to actually do the registration. We will keep that in pages/sign-in/api. Create a file called register.ts and put the following code inside: pages/sign-in/api/register.ts

import { json, type ActionFunctionArgs } from "@remix-run/node";import { POST, createUserSession } from "shared/api";export const register = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const username = formData.get("username")?.toString() ?? ""; const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users", {  body: { user: { email, password, username } }, }); if (error) {  return json({ error }, { status: 400 }); } else {  return createUserSession({   request: request,   user: data.user,   redirectTo: "/",  }); }};

pages/sign-in/index.ts

export { RegisterPage } from './ui/RegisterPage';export { register } from './api/register';

Almost done! Just need to connect the page and action to the /register route. Create register.tsx in app/routes: app/routes/register.tsx

import { RegisterPage, register } from "pages/sign-in";export { register as action };export default RegisterPage;

Now if you go to , you should be able to create a user! The rest of the application won’t react to this yet, we’ll address that momentarily. In a very similar way, we can implement the login page. Give it a try or just grab the code and move on: pages/sign-in/api/sign-in.ts

import { json, type ActionFunctionArgs } from "@remix-run/node";import { POST, createUserSession } from "shared/api";export const signIn = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users/login", {  body: { user: { email, password } }, }); if (error) {  return json({ error }, { status: 400 }); } else {  return createUserSession({   request: request,   user: data.user,   redirectTo: "/",  }); }};

pages/sign-in/ui/SignInPage.tsx

import { Form, Link, useActionData } from "@remix-run/react";import type { signIn } from "../api/sign-in";export function SignInPage() { const signInData = useActionData<typeof signIn>(); return (  <div className="auth-page">   <div className="container page">    <div className="row">     <div className="col-md-6 offset-md-3 col-xs-12">      <h1 className="text-xs-center">Sign in</h1>      <p className="text-xs-center">       <Link to="/register">Need an account?</Link>      </p>      {signInData?.error && (       <ul className="error-messages">        {signInData.error.errors.body.map((error) => (         <li key={error}>{error}</li>        ))}       </ul>      )}      <Form method="post">       <fieldset className="form-group">        <input         className="form-control form-control-lg"         name="email"         type="text"         placeholder="Email"        />       </fieldset>       <fieldset className="form-group">        <input         className="form-control form-control-lg"         name="password"         type="password"         placeholder="Password"        />       </fieldset>       <button className="btn btn-lg btn-primary pull-xs-right">        Sign in       </button>      </Form>     </div>    </div>   </div>  </div> );}

pages/sign-in/index.ts

export { RegisterPage } from './ui/RegisterPage';export { register } from './api/register';export { SignInPage } from './ui/SignInPage';export { signIn } from './api/sign-in';

app/routes/login.tsx

import { SignInPage, signIn } from "pages/sign-in";export { signIn as action };export default SignInPage;

Now let’s give the users a way to actually get to these pages.

Header

As we discussed in part 1, the app header is commonly placed either in Widgets or in Shared. We will put it in Shared because it’s very simple and all the business logic can be kept outside of it. Let’s create a place for it:

npx fsd shared ui

Now create shared/ui/Header.tsx with the following contents: shared/ui/Header.tsx

import { useContext } from "react";import { Link, useLocation } from "@remix-run/react";import { CurrentUser } from "../api/currentUser";export function Header() { const currentUser = useContext(CurrentUser); const { pathname } = useLocation(); return (  <nav className="navbar navbar-light">   <div className="container">    <Link className="navbar-brand" to="/" prefetch="intent">     conduit    </Link>    <ul className="nav navbar-nav pull-xs-right">     <li className="nav-item">      <Link       prefetch="intent"       className={`nav-link ${pathname == "/" ? "active" : ""}`}       to="/"      >       Home      </Link>     </li>     {currentUser == null ? (      <>       <li className="nav-item">        <Link         prefetch="intent"         className={`nav-link ${pathname == "/login" ? "active" : ""}`}         to="/login"        >         Sign in        </Link>       </li>       <li className="nav-item">        <Link         prefetch="intent"         className={`nav-link ${pathname == "/register" ? "active" : ""}`}         to="/register"        >         Sign up        </Link>       </li>      </>     ) : (      <>       <li className="nav-item">        <Link         prefetch="intent"         className={`nav-link ${pathname == "/editor" ? "active" : ""}`}         to="/editor"        >         <i className="ion-compose"></i>&nbsp;New Article{" "}        </Link>       </li>       <li className="nav-item">        <Link         prefetch="intent"         className={`nav-link ${pathname == "/settings" ? "active" : ""}`}         to="/settings"        >         {" "}         <i className="ion-gear-a"></i>&nbsp;Settings{" "}        </Link>       </li>       <li className="nav-item">        <Link         prefetch="intent"         className={`nav-link ${pathname.includes("/profile") ? "active" : ""}`}         to={`/profile/${currentUser.username}`}        >         {currentUser.image && (          <img           width={25}           height={25}           src={currentUser.image}           className="user-pic"           alt=""          />         )}         {currentUser.username}        </Link>       </li>      </>     )}    </ul>   </div>  </nav> );}

Export this component from shared/ui: shared/ui/index.ts

export { Header } from "./Header";

In the header, we rely on the context that’s kept in shared/api. Create that as well: shared/api/currentUser.ts

import { createContext } from "react";import type { User } from "./models";export const CurrentUser = createContext<User | null>(null);

shared/api/index.ts

export { GET, POST, PUT, DELETE } from "./client";export type { Article } from "./models";export { createUserSession, getUserFromSession, requireUser } from "./auth.server";export { CurrentUser } from "./currentUser";

Now let’s add the header to the page. We want it to be on every single page, so it makes sense to simply add it to the root route and wrap the outlet (the place where the page will be rendered) with the CurrentUser context provider. This way our entire app and also the header has access to the current user object. We will also add a loader to actually obtain the current user object from cookies. Drop the following into app/root.tsx: app/root.tsx

import { cssBundleHref } from "@remix-run/css-bundle";import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData,} from "@remix-run/react";import { Header } from "shared/ui";import { getUserFromSession, CurrentUser } from "shared/api";export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),];export const loader = ({ request }: LoaderFunctionArgs) => getUserFromSession(request);export default function App() { const user = useLoaderData<typeof loader>(); return (  <html lang="en">   <head>    <meta charSet="utf-8" />    <meta name="viewport" content="width=device-width, initial-scale=1" />    <Meta />    <Links />    <link     href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"     rel="stylesheet"     type="text/css"    />    <link     href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"     rel="stylesheet"     type="text/css"    />    <link rel="stylesheet" href="//demo.productionready.io/main.css" />    <style>{`     button {      border: 0;     }    `}</style>   </head>   <body>    <CurrentUser.Provider value={user}>     <Header />     <Outlet />    </CurrentUser.Provider>    <ScrollRestoration />    <Scripts />    <LiveReload />   </body>  </html> );}

At this point, you should end up with the following on the home page: The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.

Tabs

Now that we can detect the authentication state, let’s also quickly implement the tabs and post likes to be done with the feed page. We need another form, but this page file is getting kind of large, so let’s move these forms into adjacent files. We will create Tabs.tsx, PopularTags.tsx, and Pagination.tsx with the following content: pages/feed/ui/Tabs.tsx

import { useContext } from "react";import { Form, useSearchParams } from "@remix-run/react";import { CurrentUser } from "shared/api";export function Tabs() { const [searchParams] = useSearchParams(); const currentUser = useContext(CurrentUser); return (  <Form>   <div className="feed-toggle">    <ul className="nav nav-pills outline-active">     {currentUser !== null && (      <li className="nav-item">       <button        name="source"        value="my-feed"        className={`nav-link ${searchParams.get("source") === "my-feed" ? "active" : ""}`}       >        Your Feed       </button>      </li>     )}     <li className="nav-item">      <button       className={`nav-link ${searchParams.has("tag") || searchParams.has("source") ? "" : "active"}`}      >       Global Feed      </button>     </li>     {searchParams.has("tag") && (      <li className="nav-item">       <span className="nav-link active">        <i className="ion-pound"></i> {searchParams.get("tag")}       </span>      </li>     )}    </ul>   </div>  </Form> );}

pages/feed/ui/PopularTags.tsx

import { Form, useLoaderData } from "@remix-run/react";import { ExistingSearchParams } from "remix-utils/existing-search-params";import type { loader } from "../api/loader";export function PopularTags() { const { tags } = useLoaderData<typeof loader>(); return (  <div className="sidebar">   <p>Popular Tags</p>   <Form>    <ExistingSearchParams exclude={["tag", "page", "source"]} />    <div className="tag-list">     {tags.tags.map((tag) => (      <button       key={tag}       name="tag"       value={tag}       className="tag-pill tag-default"      >       {tag}      </button>     ))}    </div>   </Form>  </div> );}

pages/feed/ui/Pagination.tsx

import { Form, useLoaderData, useSearchParams } from "@remix-run/react";import { ExistingSearchParams } from "remix-utils/existing-search-params";import { LIMIT, type loader } from "../api/loader";export function Pagination() { const [searchParams] = useSearchParams(); const { articles } = useLoaderData<typeof loader>(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (  <Form>   <ExistingSearchParams exclude={["page"]} />   <ul className="pagination">    {Array(pageAmount)     .fill(null)     .map((_, index) =>      index + 1 === currentPage ? (       <li key={index} className="page-item active">        <span className="page-link">{index + 1}</span>       </li>      ) : (       <li key={index} className="page-item">        <button className="page-link" name="page" value={index + 1}>         {index + 1}        </button>       </li>      ),     )}   </ul>  </Form> );}

And now we can significantly simplify the feed page itself: pages/feed/ui/FeedPage.tsx

import { useLoaderData } from "@remix-run/react";import type { loader } from "../api/loader";import { ArticlePreview } from "./ArticlePreview";import { Tabs } from "./Tabs";import { PopularTags } from "./PopularTags";import { Pagination } from "./Pagination";export function FeedPage() { const { articles } = useLoaderData<typeof loader>(); return (  <div className="home-page">   <div className="banner">    <div className="container">     <h1 className="logo-font">conduit</h1>     <p>A place to share your knowledge.</p>    </div>   </div>   <div className="container page">    <div className="row">     <div className="col-md-9">      <Tabs />      {articles.articles.map((article) => (       <ArticlePreview key={article.slug} article={article} />      ))}      <Pagination />     </div>     <div className="col-md-3">      <PopularTags />     </div>    </div>   </div>  </div> );}

We also need to account for the new tab in the loader function: pages/feed/api/loader.ts

import { json, type LoaderFunctionArgs } from "@remix-run/node";import type { FetchResponse } from "openapi-fetch";import { promiseHash } from "remix-utils/promise";import { GET, requireUser } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { /* unchanged */}/** Amount of articles on one page. */export const LIMIT = 20;export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); if (url.searchParams.get("source") === "my-feed") {  const userSession = await requireUser(request);  return json(   await promiseHash({    articles: throwAnyErrors(     GET("/articles/feed", {      params: {       query: {        limit: LIMIT,        offset: !Number.isNaN(page) ? page * LIMIT : undefined,       },      },      headers: { Authorization: `Token ${userSession.token}` },     }),    ),    tags: throwAnyErrors(GET("/tags")),   }),  ); } return json(  await promiseHash({   articles: throwAnyErrors(    GET("/articles", {     params: {      query: {       tag: selectedTag,       limit: LIMIT,       offset: !Number.isNaN(page) ? page * LIMIT : undefined,      },     },    }),   ),   tags: throwAnyErrors(GET("/tags")),  }), );};

Before we leave the feed page, let’s add some code that handles likes to posts. Change your ArticlePreview.tsx to the following: pages/feed/ui/ArticlePreview.tsx

import { Form, Link } from "@remix-run/react";import type { Article } from "shared/api";interface ArticlePreviewProps { article: Article;}export function ArticlePreview({ article }: ArticlePreviewProps) { return (  <div className="article-preview">   <div className="article-meta">    <Link to={`/profile/${article.author.username}`} prefetch="intent">     <img src={article.author.image} alt="" />    </Link>    <div className="info">     <Link      to={`/profile/${article.author.username}`}      className="author"      prefetch="intent"     >      {article.author.username}     </Link>     <span className="date" suppressHydrationWarning>      {new Date(article.createdAt).toLocaleDateString(undefined, {       dateStyle: "long",      })}     </span>    </div>    <Form     method="post"     action={`/article/${article.slug}`}     preventScrollReset    >     <button      name="_action"      value={article.favorited ? "unfavorite" : "favorite"}      className={`btn ${article.favorited ? "btn-primary" : "btn-outline-primary"} btn-sm pull-xs-right`}     >      <i className="ion-heart"></i> {article.favoritesCount}     </button>    </Form>   </div>   <Link    to={`/article/${article.slug}`}    className="preview-link"    prefetch="intent"   >    <h1>{article.title}</h1>    <p>{article.description}</p>    <span>Read more...</span>    <ul className="tag-list">     {article.tagList.map((tag) => (      <li key={tag} className="tag-default tag-pill tag-outline">       {tag}      </li>     ))}    </ul>   </Link>  </div> );}

This code will send a POST request to /article/:slug with _action=favorite to mark the article as favorite. It won’t work yet, but as we start working on the article reader, we will implement this too. And with that we are officially done with the feed! Yay!

Article reader

First, we need data. Let’s create a loader:

npx fsd pages article-read -s api

pages/article-read/api/loader.ts

import { json, type LoaderFunctionArgs } from "@remix-run/node";import invariant from "tiny-invariant";import type { FetchResponse } from "openapi-fetch";import { promiseHash } from "remix-utils/promise";import { GET, getUserFromSession } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { const { data, error, response } = await responsePromise; if (error !== undefined) {  throw json(error, { status: response.status }); } return data as NonNullable<typeof data>;}export const loader = async ({ request, params }: LoaderFunctionArgs) => { invariant(params.slug, "Expected a slug parameter"); const currentUser = await getUserFromSession(request); const authorization = currentUser  ? { Authorization: `Token ${currentUser.token}` }  : undefined; return json(  await promiseHash({   article: throwAnyErrors(    GET("/articles/{slug}", {     params: {      path: { slug: params.slug },     },     headers: authorization,    }),   ),   comments: throwAnyErrors(    GET("/articles/{slug}/comments", {     params: {      path: { slug: params.slug },     },     headers: authorization,    }),   ),  }), );};

pages/article-read/index.ts

export { loader } from "./api/loader";

Now we can connect it to the route /article/:slug by creating the a route file called article.$slug.tsx: app/routes/article.$slug.tsx

export { loader } from "pages/article-read";

The page itself consists of three main blocks β€” the article header with actions (repeated twice), the article body, and the comments section. This is the markup for the page, it’s not particularly interesting: pages/article-read/ui/ArticleReadPage.tsx

import { useLoaderData } from "@remix-run/react";import type { loader } from "../api/loader";import { ArticleMeta } from "./ArticleMeta";import { Comments } from "./Comments";export function ArticleReadPage() { const { article } = useLoaderData<typeof loader>(); return (  <div className="article-page">   <div className="banner">    <div className="container">     <h1>{article.article.title}</h1>     <ArticleMeta />    </div>   </div>   <div className="container page">    <div className="row article-content">     <div className="col-md-12">      <p>{article.article.body}</p>      <ul className="tag-list">       {article.article.tagList.map((tag) => (        <li className="tag-default tag-pill tag-outline" key={tag}>         {tag}        </li>       ))}      </ul>     </div>    </div>    <hr />    <div className="article-actions">     <ArticleMeta />    </div>    <div className="row">     <Comments />    </div>   </div>  </div> );}

What’s more interesting is the ArticleMeta and Comments. They contain write operations such as liking an article, leaving a comment, etc. To get them to work, we first need to implement the backend part. Create action.ts in the api segment of the page: pages/article-read/api/action.ts

import { redirect, type ActionFunctionArgs } from "@remix-run/node";import { namedAction } from "remix-utils/named-action";import { redirectBack } from "remix-utils/redirect-back";import invariant from "tiny-invariant";import { DELETE, POST, requireUser } from "shared/api";export const action = async ({ request, params }: ActionFunctionArgs) => { const currentUser = await requireUser(request); const authorization = { Authorization: `Token ${currentUser.token}` }; const formData = await request.formData(); return namedAction(formData, {  async delete() {   invariant(params.slug, "Expected a slug parameter");   await DELETE("/articles/{slug}", {    params: { path: { slug: params.slug } },    headers: authorization,   });   return redirect("/");  },  async favorite() {   invariant(params.slug, "Expected a slug parameter");   await POST("/articles/{slug}/favorite", {    params: { path: { slug: params.slug } },    headers: authorization,   });   return redirectBack(request, { fallback: "/" });  },  async unfavorite() {   invariant(params.slug, "Expected a slug parameter");   await DELETE("/articles/{slug}/favorite", {    params: { path: { slug: params.slug } },    headers: authorization,   });   return redirectBack(request, { fallback: "/" });  },  async createComment() {   invariant(params.slug, "Expected a slug parameter");   const comment = formData.get("comment");   invariant(typeof comment === "string", "Expected a comment parameter");   await POST("/articles/{slug}/comments", {    params: { path: { slug: params.slug } },    headers: { ...authorization, "Content-Type": "application/json" },    body: { comment: { body: comment } },   });   return redirectBack(request, { fallback: "/" });  },  async deleteComment() {   invariant(params.slug, "Expected a slug parameter");   const commentId = formData.get("id");   invariant(typeof commentId === "string", "Expected an id parameter");   const commentIdNumeric = parseInt(commentId, 10);   invariant(    !Number.isNaN(commentIdNumeric),    "Expected a numeric id parameter",   );   await DELETE("/articles/{slug}/comments/{id}", {    params: { path: { slug: params.slug, id: commentIdNumeric } },    headers: authorization,   });   return redirectBack(request, { fallback: "/" });  },  async followAuthor() {   const authorUsername = formData.get("username");   invariant(    typeof authorUsername === "string",    "Expected a username parameter",   );   await POST("/profiles/{username}/follow", {    params: { path: { username: authorUsername } },    headers: authorization,   });   return redirectBack(request, { fallback: "/" });  },  async unfollowAuthor() {   const authorUsername = formData.get("username");   invariant(    typeof authorUsername === "string",    "Expected a username parameter",   );   await DELETE("/profiles/{username}/follow", {    params: { path: { username: authorUsername } },    headers: authorization,   });   return redirectBack(request, { fallback: "/" });  }, });};

Export that from the slice and then from the route. While we’re at it, let’s also connect the page itself: pages/article-read/index.ts

export { ArticleReadPage } from "./ui/ArticleReadPage";export { loader } from "./api/loader";export { action } from "./api/action";

app/routes/article.$slug.tsx

import { ArticleReadPage } from "pages/article-read";export { loader, action } from "pages/article-read";export default ArticleReadPage;

Now, even though we haven’t implemented the like button on the reader page yet, the like button in the feed will start working! That’s because it’s been sending β€œlike” requests to this route. Give that a try. ArticleMeta and Comments are, again, a bunch of forms. We’ve done this before, let’s grab their code and move on: pages/article-read/ui/ArticleMeta.tsx

import { Form, Link, useLoaderData } from "@remix-run/react";import { useContext } from "react";import { CurrentUser } from "shared/api";import type { loader } from "../api/loader";export function ArticleMeta() { const currentUser = useContext(CurrentUser); const { article } = useLoaderData<typeof loader>(); return (  <Form method="post">   <div className="article-meta">    <Link     prefetch="intent"     to={`/profile/${article.article.author.username}`}    >     <img src={article.article.author.image} alt="" />    </Link>    <div className="info">     <Link      prefetch="intent"      to={`/profile/${article.article.author.username}`}      className="author"     >      {article.article.author.username}     </Link>     <span className="date">{article.article.createdAt}</span>    </div>    {article.article.author.username == currentUser?.username ? (     <>      <Link       prefetch="intent"       to={`/editor/${article.article.slug}`}       className="btn btn-sm btn-outline-secondary"      >       <i className="ion-edit"></i> Edit Article      </Link>      &nbsp;&nbsp;      <button       name="_action"       value="delete"       className="btn btn-sm btn-outline-danger"      >       <i className="ion-trash-a"></i> Delete Article      </button>     </>    ) : (     <>      <input       name="username"       value={article.article.author.username}       type="hidden"      />      <button       name="_action"       value={        article.article.author.following         ? "unfollowAuthor"         : "followAuthor"       }       className={`btn btn-sm ${article.article.author.following ? "btn-secondary" : "btn-outline-secondary"}`}      >       <i className="ion-plus-round"></i>       &nbsp;{" "}       {article.article.author.following        ? "Unfollow"        : "Follow"}{" "}       {article.article.author.username}      </button>      &nbsp;&nbsp;      <button       name="_action"       value={article.article.favorited ? "unfavorite" : "favorite"}       className={`btn btn-sm ${article.article.favorited ? "btn-primary" : "btn-outline-primary"}`}      >       <i className="ion-heart"></i>       &nbsp; {article.article.favorited        ? "Unfavorite"        : "Favorite"}{" "}       Post{" "}       <span className="counter">        ({article.article.favoritesCount})       </span>      </button>     </>    )}   </div>  </Form> );}

pages/article-read/ui/Comments.tsx

import { useContext } from "react";import { Form, Link, useLoaderData } from "@remix-run/react";import { CurrentUser } from "shared/api";import type { loader } from "../api/loader";export function Comments() { const { comments } = useLoaderData<typeof loader>(); const currentUser = useContext(CurrentUser); return (  <div className="col-xs-12 col-md-8 offset-md-2">   {currentUser !== null ? (    <Form     preventScrollReset={true}     method="post"     className="card comment-form"    >     <div className="card-block">      <textarea       required       className="form-control"       name="comment"       placeholder="Write a comment..."       rows={3}      ></textarea>     </div>     <div className="card-footer">      <img       src={currentUser.image}       className="comment-author-img"       alt=""      />      <button       className="btn btn-sm btn-primary"       name="_action"       value="createComment"      >       Post Comment      </button>     </div>    </Form>   ) : (    <div className="row">     <div className="col-xs-12 col-md-8 offset-md-2">      <p>       <Link to="/login">Sign in</Link>       &nbsp; or &nbsp;       <Link to="/register">Sign up</Link>       &nbsp; to add comments on this article.      </p>     </div>    </div>   )}   {comments.comments.map((comment) => (    <div className="card" key={comment.id}>     <div className="card-block">      <p className="card-text">{comment.body}</p>     </div>     <div className="card-footer">      <Link       to={`/profile/${comment.author.username}`}       className="comment-author"      >       <img        src={comment.author.image}        className="comment-author-img"        alt=""       />      </Link>      &nbsp;      <Link       to={`/profile/${comment.author.username}`}       className="comment-author"      >       {comment.author.username}      </Link>      <span className="date-posted">{comment.createdAt}</span>      {comment.author.username === currentUser?.username && (       <span className="mod-options">        <Form method="post" preventScrollReset={true}>         <input type="hidden" name="id" value={comment.id} />         <button          name="_action"          value="deleteComment"          style={{           border: "none",           outline: "none",           backgroundColor: "transparent",          }}         >          <i className="ion-trash-a"></i>         </button>        </Form>       </span>      )}     </div>    </div>   ))}  </div> );}

And with that our article reader is also complete! The buttons to follow the author, like a post, and leave a comment should now function as expected. Article reader with functioning buttons to like and follow

Article editor

This is the last page that we will cover in this tutorial, and the most interesting part here is how we’re going to validate form data. The page itself, article-edit/ui/ArticleEditPage.tsx, will be quite simple, extra complexity stowed away into two other components: pages/article-edit/ui/ArticleEditPage.tsx

import { Form, useLoaderData } from "@remix-run/react";import type { loader } from "../api/loader";import { TagsInput } from "./TagsInput";import { FormErrors } from "./FormErrors";export function ArticleEditPage() { const article = useLoaderData<typeof loader>(); return (  <div className="editor-page">   <div className="container page">    <div className="row">     <div className="col-md-10 offset-md-1 col-xs-12">      <FormErrors />      <Form method="post">       <fieldset>        <fieldset className="form-group">         <input          type="text"          className="form-control form-control-lg"          name="title"          placeholder="Article Title"          defaultValue={article.article?.title}         />        </fieldset>        <fieldset className="form-group">         <input          type="text"          className="form-control"          name="description"          placeholder="What's this article about?"          defaultValue={article.article?.description}         />        </fieldset>        <fieldset className="form-group">         <textarea          className="form-control"          name="body"          rows={8}          placeholder="Write your article (in markdown)"          defaultValue={article.article?.body}         ></textarea>        </fieldset>        <fieldset className="form-group">         <TagsInput          name="tags"          defaultValue={article.article?.tagList ?? []}         />        </fieldset>        <button className="btn btn-lg pull-xs-right btn-primary">         Publish Article        </button>       </fieldset>      </Form>     </div>    </div>   </div>  </div> );}

This page gets the current article (unless we’re writing from scratch) and fills in the corresponding form fields. We’ve seen this before. The interesting part is FormErrors, because it will receive the validation result and display it to the user. Let’s take a look: pages/article-edit/ui/FormErrors.tsx

import { useActionData } from "@remix-run/react";import type { action } from "../api/action";export function FormErrors() { const actionData = useActionData<typeof action>(); return actionData?.errors != null ? (  <ul className="error-messages">   {actionData.errors.map((error) => (    <li key={error}>{error}</li>   ))}  </ul> ) : null;}

Here we are assuming that our action will return the errors field, an array of human-readable error messages. We will get to the action shortly. Another component is the tags input. It’s just a plain input field with an additional preview of chosen tags. Not much to see here: pages/article-edit/ui/TagsInput.tsx

import { useEffect, useRef, useState } from "react";export function TagsInput({ name, defaultValue,}: { name: string; defaultValue?: Array<string>;}) { const [tagListState, setTagListState] = useState(defaultValue ?? []); function removeTag(tag: string): void {  const newTagList = tagListState.filter((t) => t !== tag);  setTagListState(newTagList); } const tagsInput = useRef<HTMLInputElement>(null); useEffect(() => {  tagsInput.current && (tagsInput.current.value = tagListState.join(",")); }, [tagListState]); return (  <>   <input    type="text"    className="form-control"    id="tags"    name={name}    placeholder="Enter tags"    defaultValue={tagListState.join(",")}    onChange={(e) =>     setTagListState(e.target.value.split(",").filter(Boolean))    }   />   <div className="tag-list">    {tagListState.map((tag) => (     <span className="tag-default tag-pill" key={tag}>      <i       className="ion-close-round"       role="button"       tabIndex={0}       onKeyDown={(e) =>        [" ", "Enter"].includes(e.key) && removeTag(tag)       }       onClick={() => removeTag(tag)}      ></i>{" "}      {tag}     </span>    ))}   </div>  </> );}

Now, for the API part. The loader should look at the URL, and if it contains an article slug, that means we’re editing an existing article, and its data should be loaded. Otherwise, return nothing. Let’s create that loader: pages/article-edit/api/loader.ts

import { json, type LoaderFunctionArgs } from "@remix-run/node";import type { FetchResponse } from "openapi-fetch";import { GET, requireUser } from "shared/api";async function throwAnyErrors<T, O, Media extends `${string}/${string}`>( responsePromise: Promise<FetchResponse<T, O, Media>>,) { const { data, error, response } = await responsePromise; if (error !== undefined) {  throw json(error, { status: response.status }); } return data as NonNullable<typeof data>;}export const loader = async ({ params, request }: LoaderFunctionArgs) => { const currentUser = await requireUser(request); if (!params.slug) {  return { article: null }; } return throwAnyErrors(  GET("/articles/{slug}", {   params: { path: { slug: params.slug } },   headers: { Authorization: `Token ${currentUser.token}` },  }), );};

The action will take the new field values, run them through our data schema, and if everything is correct, commit those changes to the backend, either by updating an existing article or creating a new one: pages/article-edit/api/action.ts

import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";import { POST, PUT, requireUser } from "shared/api";import { parseAsArticle } from "../model/parseAsArticle";export const action = async ({ request, params }: ActionFunctionArgs) => { try {  const { body, description, title, tags } = parseAsArticle(   await request.formData(),  );  const tagList = tags?.split(",") ?? [];  const currentUser = await requireUser(request);  const payload = {   body: {    article: {     title,     description,     body,     tagList,    },   },   headers: { Authorization: `Token ${currentUser.token}` },  };  const { data, error } = await (params.slug   ? PUT("/articles/{slug}", {     params: { path: { slug: params.slug } },     ...payload,    })   : POST("/articles", payload));  if (error) {   return json({ errors: error }, { status: 422 });  }  return redirect(`/article/${data.article.slug ?? ""}`); } catch (errors) {  return json({ errors }, { status: 400 }); }};

The schema doubles as a parsing function for FormData, which allows us to conveniently get the clean fields or just throw the errors to handle at the end. Here’s how that parsing function could look: pages/article-edit/model/parseAsArticle.ts

export function parseAsArticle(data: FormData) { const errors = []; const title = data.get("title"); if (typeof title !== "string" || title === "") {  errors.push("Give this article a title"); } const description = data.get("description"); if (typeof description !== "string" || description === "") {  errors.push("Describe what this article is about"); } const body = data.get("body"); if (typeof body !== "string" || body === "") {  errors.push("Write the article itself"); } const tags = data.get("tags"); if (typeof tags !== "string") {  errors.push("The tags must be a string"); } if (errors.length > 0) {  throw errors; } return { title, description, body, tags: data.get("tags") ?? "" } as {  title: string;  description: string;  body: string;  tags: string; };}

Arguably, it’s a bit lengthy and repetitive, but that’s the price we pay for human-readable errors. This could also be a Zod schema, for example, but then we would have to render error messages on the frontend, and this form is not worth the complication. One last step β€” connect the page, the loader, and the action to the routes. Since we neatly support both creation and editing, we can export the same thing from both editor._index.tsx and editor.$slug.tsx: pages/article-edit/index.ts

export { ArticleEditPage } from "./ui/ArticleEditPage";export { loader } from "./api/loader";export { action } from "./api/action";

app/routes/editor._index.tsx, app/routes/editor.$slug.tsx (same content)

import { ArticleEditPage } from "pages/article-edit";export { loader, action } from "pages/article-edit";export default ArticleEditPage;

We’re done now! Log in and try creating a new article. Or β€œforget” to write the article and see the validation kick in. The Conduit article editor, with the title field saying β€œNew article” and the rest of the fields empty. Above the form there are two errors: β€œDescribe what this article is about” and β€œWrite the article itself”. The profile and settings pages are very similar to the article reader and editor, they are left as an exercise for the reader, that’s you :) Feedback

Migration from v1 to v2 | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/migration/from-v1

On this page

Why v2?

The original concept of feature-slices in 2018. Since then, many transformations of the methodology have taken place, but at the same time the basic principles were preserved: At the same time, in the previous version of the methodology, there were still weak points that

  • Sometimes it leads to boilerplate code
  • Sometimes it leads to excessive complication of the code base and non-obvious rules between abstractions
  • Sometimes it leads to implicit architectural solutions, which prevented the project from being pulled up and new people from onboarding

The new version of the methodology () is designed to eliminate these shortcomings, while preserving the existing advantages of the approach. Since 2018, another similar methodology - , which was first announced by . After merging of the two approaches, we have improved and refined existing practices - towards greater flexibility, clarity and efficiency in application.

As a result, this has even affected the name of the methodology - "feature-sliced"

Why does it make sense to migrate the project to v2?

WIP: The current version of the methodology is under development and some details may change

πŸ” More transparent and simple architecture

The methodology (v2) offers more intuitive and more common abstractions and ways of separating logic among developers. All this has an extremely positive effect on attracting new people, as well as studying the current state of the project, and distributing the business logic of the application.

πŸ“¦ More flexible and honest modularity

The methodology (v2) allows to distribute logic in a more flexible way:

  • With the ability to refactor isolated parts from scratch
  • With the ability to rely on the same abstractions, but without unnecessary interweaving of dependencies
  • With simpler requirements for the location of the new module (layer => slice => segment)

πŸš€ More specifications, plans, community

At the moment, the core-team is actively working on the latest (v2) version of the methodology So it is for her:

Of course, there will be user support for the first version as well - but the latest version is still a priority for us In the future, with the next major updates, you will still have access to the current version (v2) of the methodology, without risks for your teams and projects

Changelog

BREAKING Layers

Now the methodology assumes explicit allocation of layers at the top level

BREAKING Shared

The infrastructure abstractions /ui, /lib, /api, which used to lie in the src root of the project, are now separated by a separate directory /src/shared

NEW Entities, Processes

In v2 , other new abstractions have been added to eliminate the problems of logic complexity and high coupling.

BREAKING Abstractions & Naming

Now specific abstractions and are defined

Layers

Segments

REFINED Low coupling

Now it is much easier to between modules, thanks to the new layers. At the same time, it is still recommended to avoid as much as possible cases where it is extremely difficult to "uncouple" modules

See also

Feedback

Examples | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/examples

Small practical examples of the methodology application

Main

Feedback

Usage with NextJS | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/tech/with-nextjs

On this page It is possible to implement FSD in a NextJS project, but conflicts arise due to differences between the requirements for the NextJS project structure and the principles of FSD in two points:

  • Routing files in the pages layer
  • Conflict or absence of the app layer in NextJS

Conflict between FSD and NextJS on pages layer

NextJS suggests using the pages folder to define application routes. NextJS expects files in the pages folder to match URLs. This routing mechanism does not correspond to the FSD concept, since it is not possible to maintain a flat slice structure in such a routing mechanism.

Moving the pages folder of NextJS to the root folder of the project (recommended)

The approach is to move the pages NextJS folder to the root folder of the project and import the FSD pages into the pages NextJS folder. This saves the FSD project structure inside the src folder.

β”œβ”€β”€ pages       # NextJS pages folderβ”œβ”€β”€ srcβ”‚  β”œβ”€β”€ appβ”‚  β”œβ”€β”€ entitiesβ”‚  β”œβ”€β”€ featuresβ”‚  β”œβ”€β”€ pages     # FSD pages folderβ”‚  β”œβ”€β”€ sharedβ”‚  β”œβ”€β”€ widgets

Renaming the pages layer within the FSD structure

Another way to solve the problem is to rename the pages layer in the FSD structure to avoid conflicts with the NextJS pages folder. You can rename the pages layer in FSD to views. In that way, the structure of the project in the src folder is preserved without contradiction with the requirements of NextJS.

β”œβ”€β”€ appβ”œβ”€β”€ entitiesβ”œβ”€β”€ featuresβ”œβ”€β”€ pages       # NextJS pages folderβ”œβ”€β”€ views       # Renamed FSD pages folderβ”œβ”€β”€ sharedβ”œβ”€β”€ widgets

Keep in mind that it's highly recommended to document this rename prominently in your project's README or internal documentation. This rename is a part of your .

The absence of the app folder in NextJS

In NextJS below version 13, there is no explicit app folder, instead NextJS provides the _app.tsx file, which plays the role of a wrapping component for all project pages.

Importing app functionality to pages/_app.tsx file

To solve the problem of missing the app folder in the NextJS structure, you can create an App component inside the app layer and import the App component into pages/_app.tsx so that NextJS can work with it. For example:

// app/providers/index.tsxconst App = ({ Component, pageProps }: AppProps) => { return (  <Provider1>   <Provider2>    <BaseLayout>      <Component {...pageProps} />    </BaseLayout>   </Provider2>  </Provider1> );};export default App;

Then you can import the App component and global project styles into pages/_app.tsx as follows:

// pages/_app.tsximport 'app/styles/index.scss'export { default } from 'app/providers';

Dealing with App Router

App Router has become stable in NextJS version 13.4. App Router allows you to use the app folder for routing instead of the pages folder. To comply with the principles of FSD, you should treat the NextJS app folder in the same way as recommended to resolve a conflict with the NextJS pages folder. The approach is to move the NextJS app folder to the root folder of the project and import the FSD pages into the NextJS app folder. This saves the FSD project structure inside the src folder. You should still also add the pages folder to the root, because the App router is compatible with the Pages router.

β”œβ”€β”€ app        # NextJS app folderβ”œβ”€β”€ pages       # Stub NextJS pages folderβ”‚  β”œβ”€β”€ README.md   # Description of why this folder existsβ”œβ”€β”€ srcβ”‚  β”œβ”€β”€ app      # FSD app folderβ”‚  β”œβ”€β”€ entitiesβ”‚  β”œβ”€β”€ featuresβ”‚  β”œβ”€β”€ pages     # FSD pages folderβ”‚  β”œβ”€β”€ sharedβ”‚  β”œβ”€β”€ widgets

Middleware

If you are using middleware in a project, it also needs to be moved from src to the root of the project. Otherwise, NextJS simply won't see it β€” middleware must be located strictly at the root next to app or pages.

See also

Feedback

Migration from a custom architecture | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/migration/from-custom

On this page This guide describes an approach that might be helpful when migrating from a custom self-made architecture to Feature-Sliced Design. Here is the folder structure of a typical custom architecture. We will be using it as an example in this guide. Click on the blue arrow to open the folder. πŸ“ src

Before you start

The most important question to ask your team when considering to switch to Feature-Sliced Design is β€” do you really need it? We love Feature-Sliced Design, but even we recognize that some projects are perfectly fine without it. Here are some reasons to consider making the switch:

  1. New team members are complaining that it's hard to get to a productive level
  2. Making modifications to one part of the code often causes another unrelated part to break
  3. Adding new functionality is difficult due to the sheer amount of things you need to think about

Avoid switching to FSD against the will of your teammates, even if you are the lead. First, convince your teammates that the benefits outweigh the cost of migration and the cost of learning a new architecture instead of the established one. Also keep in mind that any kind of architectural changes are not immediately observable to the management. Make sure they are on board with the switch before starting and explain to them why it might benefit the project. tip If you need help convincing the project manager that FSD is beneficial, consider some of these points:

  1. Migration to FSD can happen incrementally, so it will not halt the development of new features
  2. A good architecture can significantly decrease the time that a new developer needs to get productive
  3. FSD is a documented architecture, so the team doesn't have to continuously spend time on maintaining their own documentation

If you made the decision to start migrating, then the first thing you want to do is to set up an alias for πŸ“ src. It will be helpful later to refer to top-level folders. We will consider @ as an alias for ./src for the rest of this guide.

Step 1. Divide the code by pages

Most custom architectures already have a division by pages, however small or large in logic. If you already have πŸ“ pages, you may skip this step. If you only have πŸ“ routes, create πŸ“ pages and try to move as much component code from πŸ“ routes as possible. Ideally, you would have a tiny route and a larger page. As you're moving code, create a folder for each page and add an index file: note For now, it's okay if your pages reference each other. You can tackle that later, but for now, focus on establishing a prominent division by pages. Route file: src/routes/products.[id].js

export { ProductPage as default } from "@/pages/product"

Page index file: src/pages/product/index.js

export { ProductPage } from "./ProductPage.jsx"

Page component file: src/pages/product/ProductPage.jsx

export function ProductPage(props) { return <div />;}

Step 2. Separate everything else from the pages

Create a folder πŸ“ src/shared and move everything that doesn't import from πŸ“ pages or πŸ“ routes there. Create a folder πŸ“ src/app and move everything that does import the pages or routes there, including the routes themselves. Remember that the Shared layer doesn't have slices, so it's fine if segments import from each other. You should end up with a file structure like this: πŸ“ src

Step 3. Tackle cross-imports between pages

Find all instances where one page is importing from the other and do one of the two things:

  1. Copy-paste the imported code into the depending page to remove the dependency
  2. Move the code to a proper segment in Shared:
    • if it's a part of the UI kit, move it to πŸ“ shared/ui;
    • if it's a configuration constant, move it to πŸ“ shared/config;
    • if it's a backend interaction, move it to πŸ“ shared/api.

note Copy-pasting isn't architecturally wrong, in fact, sometimes it may be more correct to duplicate than to abstract into a new reusable module. The reason is that sometimes the shared parts of pages start drifting apart, and you don't want dependencies getting in your way in these cases. However, there is still sense in the DRY ("don't repeat yourself") principle, so make sure you're not copy-pasting business logic. Otherwise you will need to remember to fix bugs in several places at once.

Step 4. Unpack the Shared layer

You might have a lot of stuff in the Shared layer on this step, and you generally want to avoid that. The reason is that the Shared layer may be a dependency for any other layer in your codebase, so making changes to that code is automatically more prone to unintended consequences. Find all the objects that are only used on one page and move it to the slice of that page. And yes, that applies to actions, reducers, and selectors, too. There is no benefit in grouping all actions together, but there is benefit in colocating relevant actions close to their usage. You should end up with a file structure like this: πŸ“ src

Step 5. Organize code by technical purpose

In FSD, division by technical purpose is done with segments. There are a few common ones: You can create your own segments, too, if you need. Make sure not to create segments that group code by what it is, like components, actions, types, utils. Instead, group the code by what it's for. Reorganize your pages to separate code by segments. You should already have a ui segment, now it's time to create other segments, like model for your actions, reducers, and selectors, or api for your thunks and mutations. Also reorganize the Shared layer to remove these folders:

  • πŸ“ components, πŸ“ containers β€” most of it should become πŸ“ shared/ui;
  • πŸ“ helpers, πŸ“ utils β€” if there are some reused helpers left, group them together by function, like dates or type conversions, and move theses groups to πŸ“ shared/lib;
  • πŸ“ constants β€” again, group by function and move to πŸ“ shared/config.

Optional steps

Step 6. Form entities/features from Redux slices that are used on several pages

Usually, these reused Redux slices will describe something relevant to the business, for example, products or users, so these can be moved to the Entities layer, one entity per one folder. If the Redux slice is related to an action that your users want to do in your app, like comments, then you can move it to the Features layer. Entities and features are meant to be independent from each other. If your business domain contains inherent connections between entities, refer to the for advice on how to organize these connections. The API functions related to these slices can stay in πŸ“ shared/api.

Step 7. Refactor your modules

The πŸ“ modules folder is commonly used for business logic, so it's already pretty similar in nature to the Features layer from FSD. Some modules might also be describe large chunks of the UI, like an app header. In that case, you should migrate them to the Widgets layer.

Step 8. Form a clean UI foundation in shared/ui

πŸ“ shared/ui should ideally contain a set of UI elements that don't have any business logic encoded in them. They should also be highly reusable. Refactor the UI components that used to be in πŸ“ components and πŸ“ containers to separate out the business logic. Move that business logic to the higher layers. If it's not used in too many places, you could even consider copy-pasting.

See also

Feedback

Desegemented | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/issues/desegmented

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

Situation

Very often, there is a situation on projects when modules related to a specific domain from the subject area are unnecessarily desegmented and scattered around the project

β”œβ”€β”€ components/|  β”œβ”€β”€ DeliveryCard|  β”œβ”€β”€ DeliveryChoice|  β”œβ”€β”€ RegionSelect|  β”œβ”€β”€ UserAvatarβ”œβ”€β”€ actions/|  β”œβ”€β”€ delivery.js|  β”œβ”€β”€ region.js|  β”œβ”€β”€ user.jsβ”œβ”€β”€ epics/|  β”œβ”€β”€ delivery.js|  β”œβ”€β”€ region.js|  β”œβ”€β”€ user.jsβ”œβ”€β”€ constants/|  β”œβ”€β”€ delivery.js|  β”œβ”€β”€ region.js|  β”œβ”€β”€ user.jsβ”œβ”€β”€ helpers/|  β”œβ”€β”€ delivery.js|  β”œβ”€β”€ region.js|  β”œβ”€β”€ user.jsβ”œβ”€β”€ entities/|  β”œβ”€β”€ delivery/|  |   β”œβ”€β”€ getters.js|  |   β”œβ”€β”€ selectors.js|  β”œβ”€β”€ region/|  β”œβ”€β”€ user/

Problem

The problem manifests itself at least in violation of the principle of * * High Cohesion** and excessive stretching * * of the axis of changes**

If you ignore it

Solution

Place all modules related to a specific domain/user case - directly next to each other So that when studying a particular module, all its components lie side by side, and are not scattered around the project

It also increases the discoverability and clarity of the code base and the relationships between modules

- β”œβ”€β”€ components/- |  β”œβ”€β”€ DeliveryCard- |  β”œβ”€β”€ DeliveryChoice- |  β”œβ”€β”€ RegionSelect- |  β”œβ”€β”€ UserAvatar- β”œβ”€β”€ actions/- |  β”œβ”€β”€ delivery.js- |  β”œβ”€β”€ region.js- |  β”œβ”€β”€ user.js- β”œβ”€β”€ epics/{...}- β”œβ”€β”€ constants/{...}- β”œβ”€β”€ helpers/{...} β”œβ”€β”€ entities/ |  β”œβ”€β”€ delivery/+ |  |   β”œβ”€β”€ ui/ # ~ components/+ |  |   |  β”œβ”€β”€ card.js+ |  |   |  β”œβ”€β”€ choice.js+ |  |   β”œβ”€β”€ model/+ |  |   |  β”œβ”€β”€ actions.js+ |  |   |  β”œβ”€β”€ constants.js+ |  |   |  β”œβ”€β”€ epics.js+ |  |   |  β”œβ”€β”€ getters.js+ |  |   |  β”œβ”€β”€ selectors.js+ |  |   β”œβ”€β”€ lib/ # ~ helpers |  β”œβ”€β”€ region/ |  β”œβ”€β”€ user/

See also

Feedback

Authentication | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/examples/auth

On this page Broadly, authentication consists of the following steps:

  1. Get the credentials from the user
  2. Send them to the backend
  3. Store the token to make authenticated requests

How to get credentials from the user

We are assuming that your app is responsible for getting credentials. If you have authentication via OAuth, you can simply create a login page with a link to the OAuth provider's login page and skip to .

Dedicated page for login

Usually, websites have dedicated pages for login, where you enter your username and password. These pages are quite simple, so they don't require decomposition. Login and registration forms are quite similar in appearance, so they can even be grouped into one page. Create a slice for your login/registration page on the Pages layer: Here we created two components and exported them both in the index file of the slice. These components will contain forms that are responsible for presenting the user with understandable controls to get their credentials.

Dialog for login

If your app has a dialog for login that can be used on any page, consider making that dialog a widget. That way, you can still avoid too much decomposition, but have the freedom to reuse this dialog on any page. The rest of this guide is written for the dedicated page approach, but the same principles apply to the dialog widget.

Client-side validation

Sometimes, especially for registration, it makes sense to perform client-side validation to let the user know quickly that they made a mistake. Validation can take place in the model segment of the login page. Use a schema validation library, for example, for JS/TS, and expose that schema to the ui segment: pages/login/model/registration-schema.ts

import { z } from "zod";export const registrationData = z.object({  email: z.string().email(),  password: z.string().min(6),  confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, {  message: "Passwords do not match",  path: ["confirmPassword"],});

Then, in the ui segment, you can use this schema to validate the user input: pages/login/ui/RegisterPage.tsx

import { registrationData } from "../model/registration-schema";function validate(formData: FormData) {  const data = Object.fromEntries(formData.entries());  try {    registrationData.parse(data);  } catch (error) {    // TODO: Show error message to the user  }}export function RegisterPage() {  return (    <form onSubmit={(e) => validate(new FormData(e.target))}>      <label htmlFor="email">E-mail</label>      <input id="email" name="email" required />      <label htmlFor="password">Password (min. 6 characters)</label>      <input id="password" name="password" type="password" required />      <label htmlFor="confirmPassword">Confirm password</label>      <input id="confirmPassword" name="confirmPassword" type="password" required />    </form>  )}

How to send credentials to the backend

Create a function that makes a request to your backend's login endpoint. This function can either be called directly in the component code using a mutation library (e.g. TanStack Query), or it can be called as a side effect in a state manager.

Where to store the request function

There are two places you can put this function: in shared/api, or in the api segment of the page.

In shared/api

This approach goes well with when you put all your API requests in shared/api, grouped by endpoint, for example. The file structure might look like this: The πŸ“„ client.ts file contains a wrapper around your request-making primitive (for example, fetch()). This wrapper would know about the base URL of your backend, set necessary headers, serialize data correctly, etc. shared/api/endpoints/login.ts

import { POST } from "../client";export function login({ email, password }: { email: string, password: string }) {  return POST("/login", { email, password });}

shared/api/index.ts

export { login } from "./endpoints/login";

In the api segment of the page

If you don't keep all your requests in one place, consider stashing the login request in the api segment of the login page. pages/login/api/login.ts

import { POST } from "shared/api";export function login({ email, password }: { email: string, password: string }) {  return POST("/login", { email, password });}

You don't have to export the login() function in the page's public API, because it's unlikely that any other place in the app will need this request.

Two-factor authentication

If your app supports two-factor authentication (2FA), you might have to redirect to another page where a user can enter a one-time password. Usually your POST /login request would return the user object with a flag indicating that the user has 2FA enabled. If that flag is set, redirect the user to the 2FA page. Since this page is very related to logging in, you can also keep it in the same slice, login on the Pages layer. You would also need another request function, similar to login() that we created above. Place them together, either in Shared, or in the api segment of the login page.

How to store the token for authenticated requests

Regardless of the authentication scheme you have, be it a simple login & password, OAuth, or two-factor authentication, at the end you will receive a token. This token should be stored so that subsequent requests can identify themselves. The ideal token storage for a web app is a cookie β€” it requires no manual token storage or handling. As such, cookie storage needs almost no consideration from the frontend architecture side. If your frontend framework has a server side (for example, ), then you should store the server-side cookie infrastructure in shared/api. There is an example in of how to do that with Remix. Sometimes, however, cookie storage is not an option. In this case, you will have to store the token manually. Apart from storing the token, you may also need to set up logic for refreshing your token when it expires. With FSD, there are several places where you can store the token, as well as several ways to make it available for the rest of the app.

In Shared

This approach plays well with an API client defined in shared/api because the token is freely available for other request functions that require authentication to succeed. You can make the API client hold state, either with a reactive store or simply a module-level variable, and update that state in your login()/logout() functions. Automatic token refresh can be implemented as a middleware in the API client β€” something that can execute every time you make any request. It can work like this:

  • Authenticate and store the access token as well as the refresh token
  • Make any request that requires authentication
  • If the request fails with a status code that indicates token expiration, and there is a token in the store, make a refresh request, store the new tokens, and retry the original request

One of the drawbacks of this approach is that the logic of managing and refreshing the token doesn't have a dedicated place. This can be fine for some apps or teams, but if the token management logic is more complex, it may be preferable to separate responsibilities of making requests and managing tokens. You can do that by keeping your requests and API client in shared/api, but the token store and management logic in shared/auth. Another drawback of this approach is that if your backend returns an object of your current user's information along with the token, you have to store that somewhere or discard that information and request it again from an endpoint like /me or /users/current.

In Entities

It's common for FSD projects to have an entity for a user and/or an entity for the current user. It can even be the same entity for both. note The current user is also sometimes called "viewer" or "me". This is to distinguish the single authenticated user, with permissions and private information, from a list of all users with publicly accessible information. To store the token in the User entity, create a reactive store in the model segment. That store can contain both the token and the user object. Since the API client is usually defined in shared/api or spreaded across the entities, the main challenge to this approach is making the token available to other requests that need it without breaking :

A module (file) in a slice can only import other slices when they are located on layers strictly below. There are several solutions to this challenge:

  1. Pass the token manually every time you make a request This is the simplest solution, but it quickly becomes cumbersome, and if you don't have type safety, it's easy to forget. It's also not compatible with middlewares pattern for the API client in Shared.
  2. Expose the token to the entire app with a context or a global store like localStorage The key to retrieve the token will be kept in shared/api so that the API client can access it. The reactive store of the token will be exported from the User entity, and the context provider (if needed) will be set up on the App layer. This gives more freedom for designing the API client, however, this creates an implicit dependency on higher layers to provide context. When following this approach, consider providing helpful error messages if the context or localStorage are not set up correctly.
  3. Inject the token into the API client every time it changes If your store is reactive, you can create a subscription that will update the API client's token store every time the store in the entity changes. This is similar to the previous solution in that they both create an implicit dependency on higher layers, but this one is more imperative ("push"), while the previous one is more declarative ("pull").

Once you overcome the challenge of exposing the token that is stored in the entity's model, you can encode more business logic related to token management. For example, the model segment can contain logic to invalidate the token after a certain period of time, or to refresh the token when it expires. To actually make requests to the backend, use the api segment of the User entity or shared/api.

In Pages/Widgets (not recommended)

It is discouraged to store app-wide state like an access token in pages or widgets. Avoid placing your token store in the model segment of the login page, instead choose from the first two solutions, Shared or Entities.

Logout and token invalidation

Usually, apps don't have an entire page for logging out, but the logout functionality is still very important. It consists of an authenticated request to the backend and an update to the token store. If you store all your requests in shared/api, keep the logout request function there, close to the login function. Otherwise, consider keeping the logout request function next to the button that triggers it. For example, if you have a header widget that appears on every page and contains the logout link, put that request in the api segment of that widget. The update to the token store will have to be triggered from the place of the logout button, like a header widget. You can combine the request and the store update in the model segment of that widget.

Automatic logout

Don't forget to build failsafes for when a request to log out fails, or a request to refresh a login token fails. In both of these cases, you should clear the token store. If you keep your token in Entities, this code can be placed in the model segment as it is pure business logic. If you keep your token in Shared, placing this logic in shared/api might bloat the segment and dilute its purpose. If you're noticing that your API segment contains two several unrelated things, consider splitting out the token management logic into another segment, for example, shared/auth. Feedback

Page layouts | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/examples/page-layout

On this page This guide examines the abstraction of a page layout β€” when several pages share the same overall structure, and differ only in the main content. info Is your question not covered by this guide? Post your question by leaving feedback on this article (blue button on the right) and we will consider expanding this guide!

Simple layout

The simplest layout can be seen on this page. It has a header with site navigation, two sidebars, and a footer with external links. There is no complicated business logic, and the only dynamic parts are sidebars and the switchers on the right side of the header. Such a layout can be placed entirely in shared/ui or in app/layouts, with props filling in the content for the sidebars: shared/ui/layout/Layout.tsx

import { Link, Outlet } from "react-router-dom";import { useThemeSwitcher } from "./useThemeSwitcher";export function Layout({ siblingPages, headings }) { const [theme, toggleTheme] = useThemeSwitcher(); return (  <div>   <header>    <nav>     <ul>      <li> <Link to="/">Home</Link> </li>      <li> <Link to="/docs">Docs</Link> </li>      <li> <Link to="/blog">Blog</Link> </li>     </ul>    </nav>    <button onClick={toggleTheme}>{theme}</button>   </header>   <main>    <SiblingPageSidebar siblingPages={siblingPages} />    <Outlet /> {/* This is where the main content goes */}    <HeadingsSidebar headings={headings} />   </main>   <footer>    <ul>     <li>GitHub</li>     <li>Twitter</li>    </ul>   </footer>  </div> );}

shared/ui/layout/useThemeSwitcher.ts

export function useThemeSwitcher() { const [theme, setTheme] = useState("light"); function toggleTheme() {  setTheme(theme === "light" ? "dark" : "light"); } useEffect(() => {  document.body.classList.remove("light", "dark");  document.body.classList.add(theme); }, [theme]); return [theme, toggleTheme] as const;}

The code of sidebars is left as an exercise for the reader πŸ˜‰.

Using widgets in the layout

Sometimes you want to include certain business logic in the layout, especially if you're using deeply nested routes with a router like . Then you can't store the layout in Shared or in Widgets due to :

A module in a slice can only import other slices when they are located on layers strictly below. Before we discuss solutions, we need to discuss if it's even a problem in the first place. Do you really need that layout, and if so, does it really need to be a Widget? If the block of business logic in question is reused on 2-3 pages, and the layout is simply a small wrapper for that widget, consider one of these two options:

  1. Write the layout inline on the App layer, where you configure the routing This is great for routers that support nesting, because you can group certain routes and apply the layout only to them.
  2. Just copy-paste it The urge to abstract code is often very overrated. It is especially the case for layouts, which rarely change. At some point, if one of these pages will need to change, you can simply do the change without needlessly affecting other pages. If you're worried that someone might forget to update the other pages, you can always leave a comment that describes the relationship between the pages.

If none of the above are applicable, there are two solutions to include a widget in the layout:

  1. Use render props or slots Most frameworks allow you to pass a piece of UI externally. In React, it's called , in Vue it's called .
  2. Move the layout to the App layer You can also store your layout on the App layer, for example, in app/layouts, and compose any widgets you want.

Further reading

  • There's an example of how to build a layout with authentication with React and Remix (equivalent to React Router) in the .

Feedback

Migration from v2.0 to v2.1 | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/migration/from-v2-0

On this page The main change in v2.1 is the new mental model for decomposing an interface β€” pages first. In v2.0, FSD would recommend identifying entities and features in your interface, considering even the smallest bits of entity representation and interactivity for decomposition. Then you would build widgets and pages from entities and features. In this model of decomposition, most of the logic was in entities and features, and pages were just compositional layers that didn't have much significance on their own. In v2.1, we recommend starting with pages, and possibly even stopping there. Most people already know how to separate the app into individual pages, and pages are also a common starting point when trying to locate a component in the codebase. In this new model of decomposition, you keep most of the UI and logic in each individual page, maintaining a reusable foundation in Shared. If a need arises to reuse business logic across several pages, you can move it to a layer below. Another addition to Feature-Sliced Design is the standardization of cross-imports between entities with the @x-notation.

How to migrate

There are no breaking changes in v2.1, which means that a project written with FSD v2.0 is also a valid project in FSD v2.1. However, we believe that the new mental model is more beneficial for teams and especially onboarding new developers, so we recommend making minor adjustments to your decomposition.

Merge slices

A simple way to start is by running our linter, , on the project. Steiger is built with the new mental model, and the most helpful rules will be:

  • β€” if an entity or feature is only used in one page, this rule will suggest merging that entity or feature into the page entirely.
  • β€” if a layer has too many slices, it's usually a sign that the decomposition is too fine-grained. This rule will suggest merging or grouping some slices to help project navigation.
npx steiger src

This will help you identify which slices are only used once, so that you could reconsider if they are really necessary. In such considerations, keep in mind that a layer forms some kind of global namespace for all the slices inside of it. Just as you wouldn't pollute the global namespace with variables that are only used once, you should treat a place in the namespace of a layer as valuable, to be used sparingly.

Standardize cross-imports

If you had cross-imports between in your project before (we don't judge!), you may now take advantage of a new notation for cross-importing in Feature-Sliced Design β€” the @x-notation. It looks like this: entities/B/some/file.ts

import type { EntityA } from "entities/A/@x/B";

For more details, check out the section in the reference. Feedback

Usage with NuxtJS | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/tech/with-nuxtjs

On this page It is possible to implement FSD in a NuxtJS project, but conflicts arise due to the differences between NuxtJS project structure requirements and FSD principles:

  • Initially, NuxtJS offers a project file structure without a src folder, i.e. in the root of the project.
  • The file routing is in the pages folder, while in FSD this folder is reserved for the flat slice structure.

Adding an alias for the src directory

Add an alias object to your config: nuxt.config.ts

export default defineNuxtConfig({ devtools: { enabled: true }, // Not FSD related, enabled at project startup alias: {  "@": '../src' },})

Choose how to configure the router

In NuxtJS, there are two ways to customize the routing - using a config and using a file structure. In the case of file-based routing, you will create index.vue files in folders inside the app/routes directory, and in the case of configure, you will configure the routers in the router.options.ts file.

Routing using config

In the app layer, create a router.options.ts file, and export a config object from it: app/router.options.ts

import type { RouterConfig } from '@nuxt/schema';export default <RouterConfig> { routes: (_routes) => [],};

To add a Home page to your project, you need to do the following steps:

  • Add a page slice inside the pages layer
  • Add the appropriate route to the app/router.config.ts config

To create a page slice, let's use the :

fsd pages home

Create a home-page.vue file inside the ui segment, access it using the Public API src/pages/home/index.ts

export { default as HomePage } from './ui/home-page';

Thus, the file structure will look like this:

|── srcβ”‚  β”œβ”€β”€ appβ”‚  β”‚  β”œβ”€β”€ router.config.tsβ”‚  β”œβ”€β”€ pagesβ”‚  β”‚  β”œβ”€β”€ homeβ”‚  β”‚  β”‚  β”œβ”€β”€ uiβ”‚  β”‚  β”‚  β”‚  β”œβ”€β”€ home-page.vueβ”‚  β”‚  β”‚  β”œβ”€β”€ index.ts

Finally, let's add a route to the config: app/router.config.ts

import type { RouterConfig } from '@nuxt/schema'export default <RouterConfig> { routes: (_routes) => [  {   name: 'home',   path: '/',   component: () => import('@/pages/home.vue').then(r => r.default || r)  } ],}

File Routing

First of all, create a src directory in the root of your project, and create app and pages layers inside this directory and a routes folder inside the app layer. Thus, your file structure should look like this:

β”œβ”€β”€ srcβ”‚  β”œβ”€β”€ appβ”‚  β”‚  β”œβ”€β”€ routesβ”‚  β”œβ”€β”€ pages             # Pages folder, related to FSD

In order for NuxtJS to use the routes folder inside the app layer for file routing, you need to modify nuxt.config.ts as follows: nuxt.config.ts

export default defineNuxtConfig({ devtools: { enabled: true }, // Not FSD related, enabled at project startup alias: {  "@": '../src' }, dir: {  pages: './src/app/routes' }})

Now, you can create routes for pages within app and connect pages from pages to them. For example, to add a Home page to your project, you need to do the following steps:

  • Add a page slice inside the pages layer
  • Add the corresponding route inside the app layer
  • Connect the page from the slice with the route

To create a page slice, let's use the :

fsd pages home

Create a home-page.vue file inside the ui segment, access it using the Public API src/pages/home/index.ts

export { default as HomePage } from './ui/home-page';

Create a route for this page inside the app layer:

β”œβ”€β”€ srcβ”‚  β”œβ”€β”€ appβ”‚  β”‚  β”œβ”€β”€ routesβ”‚  β”‚  β”‚  β”œβ”€β”€ index.vueβ”‚  β”œβ”€β”€ pagesβ”‚  β”‚  β”œβ”€β”€ homeβ”‚  β”‚  β”‚  β”œβ”€β”€ uiβ”‚  β”‚  β”‚  β”‚  β”œβ”€β”€ home-page.vueβ”‚  β”‚  β”‚  β”œβ”€β”€ index.ts

Add your page component inside the index.vue file: src/app/routes/index.vue

<script setup> import { HomePage } from '@/pages/home';</script><template> <HomePage/></template>

What to do with layouts?

You can place layouts inside the app layer, to do this you need to modify the config as follows: nuxt.config.ts

export default defineNuxtConfig({ devtools: { enabled: true }, // Not related to FSD, enabled at project startup alias: {  "@": '../src' }, dir: {  pages: './src/app/routes',  layouts: './src/app/layouts' }})

See also

Feedback

Usage with React Query | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/tech/with-react-query

On this page

The problem of β€œwhere to put the keys”

Solution β€” break down by entities

If the project already has a division into entities, and each request corresponds to a single entity, the purest division will be by entity. In this case, we suggest using the following structure:

└── src/                    #  β”œβ”€β”€ app/                  #  |  ...                   #  β”œβ”€β”€ pages/                 #  |  ...                   #  β”œβ”€β”€ entities/                #  |   β”œβ”€β”€ {entity}/             #  |  ...   └── api/            #  |         β”œβ”€β”€ `{entity}.query`   # Query-factory where are the keys and functions  |         β”œβ”€β”€ `get-{entity}`    # Entity getter function  |         β”œβ”€β”€ `create-{entity}`   # Entity creation function  |         β”œβ”€β”€ `update-{entity}`   # Entity update function  |         β”œβ”€β”€ `delete-{entity}`   # Entity delete function  |        ...            #  |                      #  β”œβ”€β”€ features/                #  |  ...                   #  β”œβ”€β”€ widgets/                #  |  ...                   #  └── shared/                 #    ...                   #

If there are connections between the entities (for example, the Country entity has a field-list of City entities), then you can use the or consider the alternative solution below.

Alternative solution β€” keep it in shared

In cases where entity separation is not appropriate, the following structure can be considered:

└── src/                    #  ...                     #  └── shared/                 #     β”œβ”€β”€ api/               #     ...  β”œβ”€β”€ `queries`          # Query-factories        |   β”œβ”€β”€ `document.ts`     #        |   β”œβ”€β”€ `background-jobs.ts` #        |   ...            #        └── index.ts          #

Then in @/shared/api/index.ts: @/shared/api/index.ts

export { documentQueries } from "./queries/document";

The problem of β€œWhere to insert mutations?”

It is not recommended to mix mutations with queries. There are two options:

1. Define a custom hook in the api segment near the place of use

@/features/update-post/api/use-update-title.ts

export const useUpdateTitle = () => { const queryClient = useQueryClient(); return useMutation({  mutationFn: ({ id, newTitle }) =>   apiClient    .patch(`/posts/${id}`, { title: newTitle })    .then((data) => console.log(data)),  onSuccess: (newPost) => {   queryClient.setQueryData(postsQueries.ids(id), newPost);  }, });};

2. Define a mutation function somewhere else (Shared or Entities) and use useMutation directly in the component

const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost,});

@/pages/post-create/ui/post-create-page.tsx

export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState(""); const { mutate, isPending } = useMutation({  mutationFn: postApi.createPost, }); const handleChange = (e: ChangeEvent<HTMLInputElement>) =>  setTitle(e.target.value); const handleSubmit = (e: FormEvent<HTMLFormElement>) => {  e.preventDefault();  mutate({ title, userId: DEFAULT_USER_ID }); }; return (  <form className={classes.create_form} onSubmit={handleSubmit}>   <TextField onChange={handleChange} value={title} />   <LoadingButton type="submit" variant="contained" loading={isPending}>    Create   </LoadingButton>  </form> );};

Organization of requests

Query factory

A query factory is an object where the key values are functions that return a list of query keys. Here's how to use it:

const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"],};

info queryOptions is a built-in utility in react-query@v5 (optional)

queryOptions({ queryKey, ...options,});

For greater type safety, further compatibility with future versions of react-query, and easy access to functions and query keys, you can use the built-in queryOptions function from β€œ@tanstack/react-query” .

1. Creating a Query Factory

@/entities/post/api/post.queries.ts

import { keepPreviousData, queryOptions } from "@tanstack/react-query";import { getPosts } from "./get-posts";import { getDetailPost } from "./get-detail-post";import { PostDetailQuery } from "./query/post.query";export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) =>  queryOptions({   queryKey: [...postQueries.lists(), page, limit],   queryFn: () => getPosts(page, limit),   placeholderData: keepPreviousData,  }), details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) =>  queryOptions({   queryKey: [...postQueries.details(), query?.id],   queryFn: () => getDetailPost({ id: query?.id }),   staleTime: 5000,  }),};

2. Using Query Factory in application code

import { useParams } from "react-router-dom";import { postApi } from "@/entities/post";import { useQuery } from "@tanstack/react-query";type Params = { postId: string;};export const PostPage = () => { const { postId } = useParams<Params>(); const id = parseInt(postId || ""); const {  data: post,  error,  isLoading,  isError, } = useQuery(postApi.postQueries.detail({ id })); if (isLoading) {  return <div>Loading...</div>; } if (isError || !post) {  return <>{error?.message}</>; } return (  <div>   <p>Post id: {post.id}</p>   <div>    <h1>{post.title}</h1>    <div>     <p>{post.body}</p>    </div>   </div>   <div>Owner: {post.userId}</div>  </div> );};

Benefits of using a Query Factory

  • Request structuring: A factory allows you to organize all API requests in one place, making your code more readable and maintainable.
  • Convenient access to queries and keys: The factory provides convenient methods for accessing different types of queries and their keys.
  • Query Refetching Ability: The factory allows easy refetching without the need to change query keys in different parts of the application.

Pagination

In this section, we'll look at an example of the getPosts function, which makes an API request to retrieve post entities using pagination.

1. Creating a function getPosts

The getPosts function is located in the get-posts.ts file, which is located in the api segment @/pages/post-feed/api/get-posts.ts

import { apiClient } from "@/shared/api/base";import { PostWithPaginationDto } from "./dto/post-with-pagination.dto";import { PostQuery } from "./query/post.query";import { mapPost } from "./mapper/map-post";import { PostWithPagination } from "../model/post-with-pagination";const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit);export const getPosts = async ( page: number, limit: number,): Promise<PostWithPagination> => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get<PostWithPaginationDto>("/posts", query); return {  posts: result.posts.map((post) => mapPost(post)),  limit: result.limit,  skip: result.skip,  total: result.total,  totalPages: calculatePostPage(result.total, limit), };};

2. Query factory for pagination

The postQueries query factory defines various query options for working with posts, including requesting a list of posts with a specific page and limit.

import { keepPreviousData, queryOptions } from "@tanstack/react-query";import { getPosts } from "./get-posts";export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) =>  queryOptions({   queryKey: [...postQueries.lists(), page, limit],   queryFn: () => getPosts(page, limit),   placeholderData: keepPreviousData,  }),};

3. Use in application code

@/pages/home/ui/index.tsx

export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery(  postApi.postQueries.list(page, itemsOnScreen), ); return (  <>   <Pagination    onChange={(_, page) => setPage(page)}    page={page}    count={data?.totalPages}    variant="outlined"    color="primary"   />   <Posts posts={data?.posts} />  </> );};

note The example is simplified, the full version is available on

QueryProvider for managing queries

In this guide, we will look at how to organize a QueryProvider.

1. Creating a QueryProvider

The file query-provider.tsx is located at the path @/app/providers/query-provider.tsx. @/app/providers/query-provider.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";import { ReactQueryDevtools } from "@tanstack/react-query-devtools";import { ReactNode } from "react";type Props = { children: ReactNode; client: QueryClient;};export const QueryProvider = ({ client, children }: Props) => { return (  <QueryClientProvider client={client}>   {children}   <ReactQueryDevtools />  </QueryClientProvider> );};

2. Creating a QueryClient

QueryClient is an instance used to manage API requests. The query-client.ts file is located at @/shared/api/query-client.ts. QueryClient is created with certain settings for query caching. @/shared/api/query-client.ts

import { QueryClient } from "@tanstack/react-query";export const queryClient = new QueryClient({ defaultOptions: {  queries: {   staleTime: 5 * 60 * 1000,   gcTime: 5 * 60 * 1000,  }, },});

Code generation

There are tools that can generate API code for you, but they are less flexible than the manual approach described above. If your Swagger file is well-structured, and you're using one of these tools, it might make sense to generate all the code in the @/shared/api directory.

Additional advice for organizing RQ

API Client

Using a custom API client class in the shared layer, you can standardize the configuration and work with the API in the project. This allows you to manage logging, headers and data exchange format (such as JSON or XML) from one place. This approach makes it easier to maintain and develop the project because it simplifies changes and updates to interactions with the API. @/shared/api/api-client.ts

import { API_URL } from "@/shared/config";export class ApiClient { private baseUrl: string; constructor(url: string) {  this.baseUrl = url; } async handleResponse<TResult>(response: Response): Promise<TResult> {  if (!response.ok) {   throw new Error(`HTTP error! Status: ${response.status}`);  }  try {   return await response.json();  } catch (error) {   throw new Error("Error parsing JSON response");  } } public async get<TResult = unknown>(  endpoint: string,  queryParams?: Record<string, string | number>, ): Promise<TResult> {  const url = new URL(endpoint, this.baseUrl);  if (queryParams) {   Object.entries(queryParams).forEach(([key, value]) => {    url.searchParams.append(key, value.toString());   });  }  const response = await fetch(url.toString(), {   method: "GET",   headers: {    "Content-Type": "application/json",   },  });  return this.handleResponse<TResult>(response); } public async post<TResult = unknown, TData = Record<string, unknown>>(  endpoint: string,  body: TData, ): Promise<TResult> {  const response = await fetch(`${this.baseUrl}${endpoint}`, {   method: "POST",   headers: {    "Content-Type": "application/json",   },   body: JSON.stringify(body),  });  return this.handleResponse<TResult>(response); }}export const apiClient = new ApiClient(API_URL);

See also

Feedback

Cross-imports | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/issues/cross-imports

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

Cross-imports appear when the layer or abstraction begins to take too much responsibility than it should. That is why the methodology identifies new layers that allow you to uncouple these cross-imports

See also

Feedback

Routing | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/issues/routes

On this page WIP The article is in the process of writing To bring the release of the article closer, you can:

  • πŸ“’ Share your feedback
  • πŸ’¬ Collect the relevant
  • βš’οΈ Contribute

🍰 Stay tuned!

Situation

Urls to pages are hardcoded in the layers below pages entities/post/card

<Card>  <Card.Title     href={`/post/${data.id}`}    title={data.name}  />  ...</Card>

Problem

Urls are not concentrated in the page layer, where they belong according to the scope of responsibility

If you ignore it

Then, when changing urls, you will have to keep in mind that these urls (and the logic of urls/redirects) can be in all layers except pages And it also means that now even a simple product card takes part of the responsibility from the pages, which smears the logic of the project

Solution

Determine how to work with urls/redirects from the page level and above Transfer to the layers below via composition/props/factories

See also

Feedback

Usage with SvelteKit | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/tech/with-sveltekit

On this page It is possible to implement FSD in a SvelteKit project, but conflicts arise due to the differences between the structure requirements of a SvelteKit project and the principles of FSD:

  • Initially, SvelteKit offers a file structure inside the src/routes folder, while in FSD the routing must be part of the app layer.
  • SvelteKit suggests putting everything not related to routing in the src/lib folder.

Let's set up the config

svelte.config.ts

import adapter from '@sveltejs/adapter-auto';import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';/** @type {import('@sveltejs/kit').Config}*/const config = { preprocess: [vitePreprocess()], kit: {  adapter: adapter(),  files: {   routes: 'src/app/routes',       // move routing inside the app layer   lib: 'src',   appTemplate: 'src/app/index.html',  // Move the application entry point inside the app layer   assets: 'public'  },  alias: {   '@/*': 'src/*'            // Create an alias for the src directory  } }};export default config;

Move file routing to src/app.

Let's create an app layer, move the app's entry point index.html into it, and create a routes folder. Thus, your file structure should look like this:

β”œβ”€β”€ srcβ”‚  β”œβ”€β”€ appβ”‚  β”‚  β”œβ”€β”€ index.htmlβ”‚  β”‚  β”œβ”€β”€ routesβ”‚  β”œβ”€β”€ pages                # FSD Pages folder

Now, you can create routes for pages within app and connect pages from pages to them. For example, to add a home page to your project, you need to do the following steps:

  • Add a page slice inside the pages layer
  • Add the corresponding rooute to the routes folder from the app layer
  • Align the page from the slice with the rooute

To create a page slice, let's use the :

fsd pages home

Create a home-page.svelte file inside the ui segment, access it using the Public API src/pages/home/index.ts

export { default as HomePage } from './ui/home-page.svelte';

Create a route for this page inside the app layer:

β”œβ”€β”€ srcβ”‚  β”œβ”€β”€ appβ”‚  β”‚  β”œβ”€β”€ routesβ”‚  β”‚  β”‚  β”œβ”€β”€ +page.svelteβ”‚  β”‚  β”œβ”€β”€ index.htmlβ”‚  β”œβ”€β”€ pagesβ”‚  β”‚  β”œβ”€β”€ homeβ”‚  β”‚  β”‚  β”œβ”€β”€ uiβ”‚  β”‚  β”‚  β”‚  β”œβ”€β”€ home-page.svelteβ”‚  β”‚  β”‚  β”œβ”€β”€ index.ts

Add your page component inside the +page.svelte file: src/app/routes/+page.svelte

<script> import { HomePage } from '@/pages/home';</script><HomePage/>

See also.

Feedback

Types | Feature-Sliced Design

Source: https://feature-sliced.design/docs/guides/examples/types

On this page This guide concerns data types from typed languages like TypeScript and describes where they fit within FSD. info Is your question not covered by this guide? Post your question by leaving feedback on this article (blue button on the right) and we will consider expanding this guide!

Utility types

Utility types are types that don't have much meaning on their own and are usually used with other types. For example:

type ArrayValues<T extends readonly unknown[]> = T[number];

Source: To make utility types available across your project, either install a library like , or create your own library in shared/lib. Make sure to clearly indicate what new types should be added to this library, and what types don't belong there. For example, call it shared/lib/utility-types and add a README inside that describes what is a utility type in your team. Don't overestimate the potential reusability of a utility type. Just because it can be reused, doesn't mean it will be, and as such, not every utility type needs to be in Shared. Some utility types are fine right next to where they are needed: warning Resist the temptation to create a shared/types folder, or to add a types segment to your slices. The category "types" is similar to the category "components" or "hooks" in that it describes what the contents are, not what they are for. Segments should describe the purpose of the code, not the essence.

Business entities and their cross-references

Among the most important types in an app are the types of business entities, i.e. the real-world things that your app works with. For example, in a music streaming app, you might have business entities Song, Album, etc. Business entities often come from the backend, so the first step is to type the backend responses. It's convenient to have a function to make a request to every endpoint, and to type the response of this function. For extra type safety, you may want to run the response through a schema validation library like . For example, if you keep all your requests in Shared, you could do it like this: shared/api/songs.ts

import type { Artist } from "./artists";interface Song { id: number; title: string; artists: Array<Artist>;}export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise<Array<Song>>);}

You might notice that the Song type references a different entity, Artist. This is a benefit of storing your requests in Shared β€” real-world types are often intertwined. If we kept this function in entities/song/api, we wouldn't be able to simply import Artist from entities/artist, because FSD restricts cross-imports between slices with :

A module in a slice can only import other slices when they are located on layers strictly below. There are two ways to deal with this issue:

  1. Parametrize your types You can make your types accept type arguments as slots for connections with other entities, and even impose constraints on those slots. For example: entities/song/model/song.ts
interface Song<ArtistType extends { id: string }> { id: number; title: string; artists: Array<ArtistType>;}

This works better for some types than others. A simple type like Cart = { items: Array } can easily be made to work with any type of product. More connected types, like Country and City, may not be as easy to separate. 2. Cross-import (but do it right) To make cross-imports between entities in FSD, you can use a special public API specifically for each slice that will be cross-importing. For example, if we have entities song, artist, and playlist, and the latter two need to reference song, we can make two special public APIs for both of them in the song entity with the @x notation: The contents of a file πŸ“„ entities/song/@x/artist.ts are similar to πŸ“„ entities/song/index.ts: entities/song/@x/artist.ts

export type { Song } from "../model/song.ts";

Then the πŸ“„ entities/artist/model/artist.ts can import Song like this: entities/artist/model/artist.ts

import type { Song } from "entities/song/@x/artist";export interface Artist { name: string; songs: Array<Song>;}

By making explicit connections between entities, we stay on top of inter-dependencies and maintain a decent level of domain separation.

Data transfer objects and mappers

Data transfer objects, or DTOs, is a term that describes the shape of data that comes from the backend. Sometimes, the DTO is fine to use as is, but sometimes it's inconvenient for the frontend. That's where mappers come in β€” they transform a DTO into a more convenient shape.

Where to put DTOs

If you have backend types in a separate package (for example, if you share code between the frontend and the backend), then just import your DTOs from there and you're done! If you don't share code between the backend and frontend, then you need to keep DTOs somewhere in your frontend codebase, and we will explore this case below. If you have your request functions in shared/api, that's where the DTOs should be, right next to the function that uses them: shared/api/songs.ts

import type { ArtistDTO } from "./artists";interface SongDTO { id: number; title: string; artist_ids: Array<ArtistDTO["id"]>;}export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise<Array<SongDTO>>);}

As mentioned in the previous section, storing your requests and DTOs in Shared comes with the benefit of being able to reference other DTOs.

Where to put mappers

Mappers are functions that accept a DTO for transformation, and as such, they should be located near the definition of the DTO. In practice this means that if your requests and DTOs are defined in shared/api, then the mappers should go there as well: shared/api/songs.ts

import type { ArtistDTO } from "./artists";interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array<ArtistDTO["id"]>;}interface Song { id: string; title: string; /** The full title of the song, including the disc number. */ fullTitle: string; artistIds: Array<string>;}function adaptSongDTO(dto: SongDTO): Song { return {  id: String(dto.id),  title: dto.title,  fullTitle: `${dto.disc_no} / ${dto.title}`,  artistIds: dto.artist_ids.map(String), };}export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));}

If your requests and stores are defined in entity slices, then all this code would go there, keeping in mind the limitations of cross-imports between slices: entities/song/api/dto.ts

import type { ArtistDTO } from "entities/artist/@x/song";export interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array<ArtistDTO["id"]>;}

entities/song/api/mapper.ts

import type { SongDTO } from "./dto";export interface Song { id: string; title: string; /** The full title of the song, including the disc number. */ fullTitle: string; artistIds: Array<string>;}export function adaptSongDTO(dto: SongDTO): Song { return {  id: String(dto.id),  title: dto.title,  fullTitle: `${dto.disc_no} / ${dto.title}`,  artistIds: dto.artist_ids.map(String), };}

entities/song/api/listSongs.ts

import { adaptSongDTO } from "./mapper";export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));}

entities/song/model/songs.ts

import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";import { listSongs } from "../api/listSongs";export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs);const songAdapter = createEntityAdapter();const songsSlice = createSlice({ name: "songs", initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => {  builder.addCase(fetchSongs.fulfilled, (state, action) => {   songAdapter.upsertMany(state, action.payload);  }) },});

How to deal with nested DTOs

The most problematic part is when a response from the backend contains several entities. For example, if the song included not just the authors' IDs, but the entire author objects. In this case, it is impossible for entities not to know about each other (unless we want to discard the data or have a firm conversation with the backend team). Instead of coming up with solutions for indirect connections between slices (such as a common middleware that would dispatch actions to other slices), prefer explicit cross-imports with the @x notation. Here is how we can implement it with Redux Toolkit: entities/song/model/songs.ts

import { createSlice, createEntityAdapter, createAsyncThunk, createSelector,} from '@reduxjs/toolkit'import { normalize, schema } from 'normalizr'import { getSong } from "../api/getSong";// Define normalizr entity schemasexport const artistEntity = new schema.Entity('artists')export const songEntity = new schema.Entity('songs', { artists: [artistEntity],})const songAdapter = createEntityAdapter()export const fetchSong = createAsyncThunk( 'songs/fetchSong', async (id: string) => {  const data = await getSong(id)  // Normalize the data so reducers can load a predictable payload, like:  // `action.payload = { songs: {}, artists: {} }`  const normalized = normalize(data, songEntity)  return normalized.entities })export const slice = createSlice({ name: 'songs', initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => {  builder.addCase(fetchSong.fulfilled, (state, action) => {   songAdapter.upsertMany(state, action.payload.songs)  }) },})const reducer = slice.reducerexport default reducer

entities/song/@x/artist.ts

export { fetchSong } from "../model/songs";

entities/artist/model/artists.ts

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'import { fetchSong } from 'entities/song/@x/artist'const artistAdapter = createEntityAdapter()export const slice = createSlice({ name: 'users', initialState: artistAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => {  builder.addCase(fetchSong.fulfilled, (state, action) => {   // And handle the same fetch result by inserting the artists here   artistAdapter.upsertMany(state, action.payload.artists)  }) },})const reducer = slice.reducerexport default reducer

This slightly limits the benefits of slice isolation, but it accurately represents a connection between these two entities that we have no control over. If these entities are to ever be refactored, they have to be refactored together.

Global types and Redux

Global types are types that will be used across the whole application. There are two kinds of global types, based on what they need to know about:

  1. Generic types that don't have any application specifics
  2. Types that need to know about the whole application

The first case is simple to resolve β€” place your types in Shared, in an appropriate segment. For example, if you have an interface for a global variable for analytics, you can put it in shared/analytics. warning Avoid creating the shared/types folder. It groups unrelated things based only on the property of "being a type", and that property is usually not useful when searching for code in a project. The second case is commonly encountered in projects with Redux without RTK. Your final store type is only available once you add all the reducers together, but this store type needs to be available to selectors that you use across the app. For example, here's your typical store definition: app/store/index.ts

import { combineReducers, rootReducer } from "redux";import { songReducer } from "entities/song";import { artistReducer } from "entities/artist";const rootReducer = combineReducers(songReducer, artistReducer);const store = createStore(rootReducer);type RootState = ReturnType<typeof rootReducer>;type AppDispatch = typeof store.dispatch;

It would be nice to have typed Redux hooks useAppDispatch and useAppSelector in shared/store, but they cannot import RootState and AppDispatch from the App layer due to the :

A module in a slice can only import other slices when they are located on layers strictly below. The recommended solution in this case is to create an implicit dependency between layers Shared and App. These two types, RootState and AppDispatch are unlikely to change, and they will be familiar to Redux developers, so we don't have to worry about them as much. In TypeScript, you can do it by declaring the types as global like this: app/store/index.ts

/* same content as in the code block before… */declare type RootState = ReturnType<typeof rootReducer>;declare type AppDispatch = typeof store.dispatch;

shared/store/index.ts

import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux";export const useAppDispatch = useDispatch.withTypes<AppDispatch>()export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Enums

The general rule with enums is that they should be defined as close to the usage locations as possible. When an enum represents values specific to a single feature, it should be defined in that same feature. The choice of segment should be dictated by usage locations as well. If your enum contains, for example, positions of a toast on the screen, it should be placed in the ui segment. If it represents the loading state of a backend operation, it should be placed in the api segment. Some enums are truly common across the whole project, like general backend response statuses or design system tokens. In this case, you can place them in Shared, and choose the segment based on what the enum represents (api for response statuses, ui for design tokens, etc.).

Type validation schemas and Zod

If you want to validate that your data conforms to a certain shape or constraints, you can define a validation schema. In TypeScript, a popular library for this job is . Validation schemas should also be colocated with the code that uses them, as much as possible. Validation schemas are similar to mappers (as discussed in the section) in the sense that they take a data transfer object and parse it, producing an error if the parsing fails. One of the most common cases of validation is for the data that comes from the backend. Typically, you want to fail the request when the data doesn't match the schema, so it makes sense to put the schema in the same place as the request function, which is usually the api segment. If your data comes through user input, like a form, the validation should happen as the data is being entered. You can place your schema in the ui segment, next to the form component, or in the model segment, if the ui segment is too crowded.

Typings of component props and context

In general, it's best to keep the props or context interface in the same file as the component or context that uses them. If you have a framework with single-file components, like Vue or Svelte, and you can't define the props interface in the same file, or you want to share that interface between several components, create a separate file in the same folder, typically, the ui segment. Here's an example with JSX (React or Solid): pages/home/ui/RecentActions.tsx

interface RecentActionsProps { actions: Array<{ id: string; text: string }>;}export function RecentActions({ actions }: RecentActionsProps) { /* … */}

And here's an example with the interface stored in a separate file for Vue: pages/home/ui/RecentActionsProps.ts

export interface RecentActionsProps { actions: Array<{ id: string; text: string }>;}

pages/home/ui/RecentActions.vue

<script setup lang="ts"> import type { RecentActionsProps } from "./RecentActionsProps"; const props = defineProps<RecentActionsProps>();</script>

Ambient declaration files (*.d.ts)

Some packages, for example, or , require ambient declaration files to work across your app. Usually, they aren't large or complicated, so they often don't require any architecting, it's fine to just throw them in the src/ folder. To keep the src more organized, you can keep them on the App layer, in app/ambient/. Other packages simply don't have typings, and you might want to declare them as untyped or even write your own typings for them. A good place for those typings would be shared/lib, in a folder like shared/lib/untyped-packages. Create a %LIBRARY_NAME%.d.ts file there and declare the types you need: shared/lib/untyped-packages/use-react-screenshot.d.ts

// This library doesn't have typings, and we didn't want to bother writing our own.declare module "use-react-screenshot";

Auto-generation of types

It's common to generate types from external sources, for example, generating backend types from an OpenAPI schema. In this case, create a dedicated place in your codebase for these types, like shared/api/openapi. Ideally, you should also include a README in that folder that describes what these files are, how to regenerate them, etc. Feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment