@@ -1,8529 +0,0 @@
1<SYSTEM>This is the full developer documentation for Feature-Sliced Design</SYSTEM>
2
3# Feature-Sliced Design
4
5# A clearer structure for frontend codebases
6
7Rules and conventions for organizing front-end code to keep projects clear and stable as requirements change.
8
9[Get Started ](/docs/get-started/overview/)
10
11[ Community](https://t.me/feature_sliced)
12
13[Join active discussions around real FSD questions, with community help and FastTrack Bot support.](https://t.me/feature_sliced)
14
15[ Steiger](https://github.com/feature-sliced/steiger)
16
17[Turn FSD rules into automated checks for code structure.](https://github.com/feature-sliced/steiger)
18
19[ Docs for LLMs](/docs/llms)
20
21[FSD docs and resources for LLM-powered tools and AI use cases.](/docs/llms)
22
23## Popular guides
24
25[](/docs/reference/layers)
26
27### [Layers](/docs/reference/layers)
28
29[Understand how layers structure your app](/docs/reference/layers)
30
31[](/docs/guides/migration/from-custom)
32
33### [From a custom architecture](/docs/guides/migration/from-custom)
34
35[Migrate your existing project to FSD incrementally](/docs/guides/migration/from-custom)
36
37[](/docs/reference/public-api)
38
39### [Public API](/docs/reference/public-api)
40
41[Define clear boundaries between slices](/docs/reference/public-api)
42
43[](/docs/guides/issues/cross-imports)
44
45### [Cross-imports](/docs/guides/issues/cross-imports)
46
47[Avoid the most common architectural violation](/docs/guides/issues/cross-imports)
48
49[](/docs/guides/issues/excessive-entities)
50
51### [Excessive Entities](/docs/guides/issues/excessive-entities)
52
53[Avoid over-splitting your domain into too many entities](/docs/guides/issues/excessive-entities)
54
55[](/docs/get-started/faq)
56
57### [FAQ](/docs/get-started/faq)
58
59[Find answers about adopting FSD in practice](/docs/get-started/faq)
60
61# Alternatives
62
63History of architecture approaches
64
65## Big Ball of Mud
66
67[Section titled “Big Ball of Mud”](#big-ball-of-mud)
68
69> What is it; Why is it so common; When it starts to bring problems; What to do and how does FSD help in this
70
71* [(Article) Oleg Isonen - Last words on UI architecture before an AI takes over](https://oleg008.medium.com/last-words-on-ui-architecture-before-an-ai-takes-over-468c78f18f0d)
72* [(Report) Julia Nikolaeva, iSpring - Big Ball of Mud and other problems of the monolith, we have handled](http://youtu.be/gna4Ynz1YNI)
73* [(Article) DD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/)
74
75## Smart & Dumb components
76
77[Section titled “Smart & Dumb components”](#smart--dumb-components)
78
79> About the approach; About applicability in the frontend; Methodology position
80
81About obsolescence, about a new view from the methodology
82
83Why component-containers approach is evil?
84
85* [(Article) Den Abramov-Presentation and Container Components (TLDR: deprecated)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
86
87## Design Principles
88
89[Section titled “Design Principles”](#design-principles)
90
91> What are we talking about; FSD position
92
93SOLID, GRASP, KISS, YAGNI, … - and why they don’t work well together in practice
94
95And how does it aggregate these practices
96
97* [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Design Principles)](https://youtu.be/SnzPAr_FJ7w?t=380)
98
99## DDD
100
101[Section titled “DDD”](#ddd)
102
103> About the approach; Why does it work poorly in practice
104
105What is the difference, how does it improve applicability, where does it adopt practices
106
107* [(Article) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
108* [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528)
109
110## Clean Architecture
111
112[Section titled “Clean Architecture”](#clean-architecture)
113
114> About the approach; About applicability in the frontend; FSD position
115
116How are they similar (to many), how are they different
117
118* [(Thread) About use-case/interactor in the methodology](https://t.me/feature_sliced/3897)
119* [(Thread) About DI in the methodology](https://t.me/feature_sliced/4592)
120* [(Article) Alex Bespoyasov - Clean Architecture on frontend](https://bespoyasov.me/blog/clean-architecture-on-frontend/)
121* [(Article) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
122* [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528)
123* [(Article) Misconceptions of Clean Architecture](http://habr.com/ru/company/mobileup/blog/335382/)
124
125## Frameworks
126
127[Section titled “Frameworks”](#frameworks)
128
129> About applicability in the frontend; Why frameworks do not solve problems; why there is no single approach; FSD position
130
131Framework-agnostic, conventional-approach
132
133* [(Article) About the reasons for creating the methodology (fragment about frameworks)](/docs/about/motivation)
134* [(Thread) About the applicability of the methodology for different frameworks](https://t.me/feature_sliced/3867)
135
136## Atomic Design
137
138[Section titled “Atomic Design”](#atomic-design)
139
140### What is it?
141
142[Section titled “What is it?”](#what-is-it)
143
144In Atomic Design, the scope of responsibility is divided into standardized layers.
145
146Atomic Design is broken down into **5 layers** (from top to bottom):
147
1481. `pages` - Functionality similar to the `pages` layer in FSD.
1492. `templates` - Components that define the structure of a page without tying to specific content.
1503. `organisms` - Modules consisting of molecules that have business logic.
1514. `molecules` - More complex components that generally do not contain business logic.
1525. `atoms` - UI components without business logic.
153
154Modules 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.
155
156### Applicability to frontend
157
158[Section titled “Applicability to frontend”](#applicability-to-frontend)
159
160Atomic 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.
161
162However, since Atomic Design focuses on UI components and their composition, a problem arises with implementing business logic within the architecture.
163
164The 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.
165
166### How does it relate to FSD?
167
168[Section titled “How does it relate to FSD?”](#how-does-it-relate-to-fsd)
169
170In 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.
171
172* shared/
173
174 * ui/
175
176 * atoms/
177
178 * …
179
180 * molecules/
181
182 * …
183
184A 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.
185
186* [Atomic Design Methodology](https://atomicdesign.bradfrost.com/table-of-contents/)
187* [(Thread) About applicability in shared / ui](https://t.me/feature_sliced/1653)
188* [(Video) Briefly about Atomic Design](https://youtu.be/Yi-A20x2dcA)
189* [(Talk) Ilya Azin - Feature-Sliced Design (fragment about Atomic Design)](https://youtu.be/SnzPAr_FJ7w?t=587)
190
191## Feature Driven
192
193[Section titled “Feature Driven”](#feature-driven)
194
195> About the approach; About applicability in the frontend; FSD position
196
197About compatibility, historical development and comparison
198
199* [(Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs)
200* [Feature Driven-Short specification (from the point of view of FSD)](https://github.com/feature-sliced/documentation/tree/rc/feature-driven)
201
202# Mission
203
204Here we describe the goals and limitations of the applicability of the methodology-which we are guided by when developing the methodology
205
206* We see our goal as a balance between ideology and simplicity
207* We won’t be able to make a silver bullet that fits everyone
208
209**Nevertheless, the methodology should be close and accessible to a fairly wide range of developers**
210
211## Goals
212
213[Section titled “Goals”](#goals)
214
215### Intuitive clarity for a wide range of developers
216
217[Section titled “Intuitive clarity for a wide range of developers”](#intuitive-clarity-for-a-wide-range-of-developers)
218
219The methodology should be accessible - for most of the team in projects
220
221*Because even with all the future tools , it will not be enough, if only experienced seniors/leads will understand the methodology*
222
223### Solving everyday problems
224
225[Section titled “Solving everyday problems”](#solving-everyday-problems)
226
227The methodology should set out the reasons and solutions to our everyday problems when developing projects
228
229**And also-attach tools to all this (cli, linters)**
230
231So that developers can use a *battle-tested* approach that allows them to bypass long-standing problems of architecture and development
232
233> *@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.*
234
235## Limitations
236
237[Section titled “Limitations”](#limitations)
238
239We 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*
240
241Everyone has their own level of experience in designing and developing systems, **therefore, it is worth understanding the following:**
242
243* **Will not work**: very simple, very clear, for everyone
244
245 > *@sergeysova: Some concepts cannot be intuitively understood until you encounter problems and spend years solving them.*
246 >
247 > * *In math world: is graph theory.*
248 > * *In physics: quantum mechanics.*
249 > * *In programming: application architecture.*
250
251* **Possible and desirable**: simplicity, extensibility
252
253## See also
254
255[Section titled “See also”](#see-also)
256
257* [Architecture problems](/docs/about/understanding/architecture#problems)
258
259# Motivation
260
261The main idea of **Feature-Sliced Design** is to facilitate and reduce the cost of developing complex and developing projects, based on [combining research results, discussing the experience of various kinds of a wide range of developers](https://github.com/feature-sliced/documentation/discussions).
262
263Obviously, this will not be a silver bullet, and of course, the methodology will have its own [limits of applicability](/docs/about/mission).
264
265Nevertheless, there are reasonable questions regarding *the feasibility of such a methodology as a whole*
266
267Note
268
269More details [discussed in the discussion](https://github.com/feature-sliced/documentation/discussions/27)
270
271## Why are there not enough existing solutions?
272
273[Section titled “Why are there not enough existing solutions?”](#why-are-there-not-enough-existing-solutions)
274
275> It usually, these arguments:
276>
277> * *“Why you need some new methodology, if you already have long-established approaches and principles of design such as `SOLID`, `KISS`, `YAGNI`, `DDD`, `GRASP`, `DRY`, etc.”*
278> * *“All the problems are solved by good project documentation, tests, and structured processes”*
279> * *“Problems would not have happened if all developers are following all the above”*
280> * *“Everything was invented before you, you just can’t use it”*
281> * *“Take {FRAMEWORK\_NAME} - everything has already been decided for you there”*
282
283### Principles alone are not enough
284
285[Section titled “Principles alone are not enough”](#principles-alone-are-not-enough)
286
287**The existence of principles alone is not enough to design a good architecture**
288
289Not everyone knows them completely, even fewer understand and apply them correctly
290
291*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?”*
292
293### Processes don’t always work
294
295[Section titled “Processes don’t always work”](#processes-dont-always-work)
296
297*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**
298
299* The time of entry of each developer into the project is not greatly reduced, because the documentation will most often come out huge / outdated
300* Constantly make sure that everyone understands architecture in the same way-it also requires a huge amount of resources
301* Do not forget about the bus-factor
302
303### Existing frameworks cannot be applied everywhere
304
305[Section titled “Existing frameworks cannot be applied everywhere”](#existing-frameworks-cannot-be-applied-everywhere)
306
307* Existing solutions usually have a high entry threshold, which makes it difficult to find new developers
308* 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**
309
310> 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?”*
311
312### As a result
313
314[Section titled “As a result”](#as-a-result)
315
316We 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
317
318> @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.”*
319
320## Why do developers need the methodology?
321
322[Section titled “Why do developers need the methodology?”](#why-do-developers-need-the-methodology)
323
324### Focus on business features, not on architecture problems
325
326[Section titled “Focus on business features, not on architecture problems”](#focus-on-business-features-not-on-architecture-problems)
327
328The 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.
329
330*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*
331
332### An experience-proven solution
333
334[Section titled “An experience-proven solution”](#an-experience-proven-solution)
335
336The methodology is designed for developers who are aimed at *a proven solution for designing complex business logic*
337
338*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*
339
340### Project Health
341
342[Section titled “Project Health”](#project-health)
343
344The methodology will allow *to solve and track the problems of the project in advance, without requiring a huge amount of resources*
345
346**Most often, technical debt accumulates and accumulates over time, and the responsibility for its resolution lies on both the lead and the team**
347
348The methodology will allow you to *warn* possible problems in the scaling and development of the project in advance
349
350## Why does a business need a methodology?
351
352[Section titled “Why does a business need a methodology?”](#why-does-a-business-need-a-methodology)
353
354### Fast onboarding
355
356[Section titled “Fast onboarding”](#fast-onboarding)
357
358With the methodology, you can hire a person to the project who **is already previously familiar with this approach, and not train again**
359
360*People start to understand and benefit the project faster, and there are additional guarantees to find people for the next iterations of the project*
361
362### An experience-proven solution
363
364[Section titled “An experience-proven solution”](#an-experience-proven-solution-1)
365
366With the methodology, the business will get *a solution for most of the issues that arise during the development of systems*
367
368Since 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
369
370### Applicability for different stages of the project
371
372[Section titled “Applicability for different stages of the project”](#applicability-for-different-stages-of-the-project)
373
374The methodology can benefit the project *both at the stage of project support and development, and at the MVP stage*
375
376Yes, 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”)
377
378*The same can be said about testing*
379
380## When is our methodology not needed?
381
382[Section titled “When is our methodology not needed?”](#when-is-our-methodology-not-needed)
383
384* If the project will live for a short time
385* If the project does not need a supported architecture
386* If the business does not perceive the connection between the code base and the speed of feature delivery
387* If it is more important for the business to close orders as soon as possible, without further support
388
389### Business Size
390
391[Section titled “Business Size”](#business-size)
392
393* **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
394* **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)
395* **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
396
397## Plans
398
399[Section titled “Plans”](#plans)
400
401The main part of the goals [is set out here](/docs/about/mission#goals), but in addition, it is worth talking about our expectations from the methodology in the future
402
403### Combining experience
404
405[Section titled “Combining experience”](#combining-experience)
406
407Now we are trying to combine all our diverse experience of the `core-team`, and get a methodology hardened by practice as a result
408
409Of 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**
410
411*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)*
412
413### Life outside the specification
414
415[Section titled “Life outside the specification”](#life-outside-the-specification)
416
417If everything goes well, then the methodology will not be limited only to the specification and the toolkit
418
419* Perhaps there will be reports, articles
420* There may be `CODE_MODEs` for migrations to other technologies of projects written according to the methodology
421* It is possible that as a result we will be able to reach the maintainers of large technological solutions
422 * *Especially for React, compared to other frameworks - this is the main problem, because it does not say how to solve certain problems*
423
424## See also
425
426[Section titled “See also”](#see-also)
427
428* [(Discussion) Don’t need a methodology?](https://github.com/feature-sliced/documentation/discussions/27)
429* [About the methodology’s mission: goals and limitations](/docs/about/mission)
430* [Types of knowledge in the project](/docs/about/understanding/knowledge-types)
431
432# Promote in company
433
434## Do the project and the company need a methodology?
435
436[Section titled “Do the project and the company need a methodology?”](#do-the-project-and-the-company-need-a-methodology)
437
438> About the justification of the application, Those duty
439
440## How can I submit a methodology to a business?
441
442[Section titled “How can I submit a methodology to a business?”](#how-can-i-submit-a-methodology-to-a-business)
443
444## How to prepare and justify a plan to move to the methodology?
445
446[Section titled “How to prepare and justify a plan to move to the methodology?”](#how-to-prepare-and-justify-a-plan-to-move-to-the-methodology)
447
448# Promote in team
449
450* Onboard newcomers
451* Development Guidelines (“where to search N module”, etc…)
452* New approach for tasks
453
454## See also
455
456[Section titled “See also”](#see-also)
457
458* [(Thread) The simplicity of the old approaches and the importance of mindfulness](https://t.me/feature_sliced/3360)
459* [(Thread) About the convenience of searching by layers](https://t.me/feature_sliced/1918)
460
461# Integration aspects
462
463## Summary
464
465[Section titled “Summary”](#summary)
466
467First 5 minutes (RU):
468
469[YouTube video player](https://www.youtube.com/embed/TFA6zRO_Cl0?start=2110)
470
471## Also
472
473[Section titled “Also”](#also)
474
475**Advantages**:
476
477* [Overview](/docs/get-started/overview)
478* CodeReview
479* Onboarding
480
481**Disadvantages:**
482
483* Mental complexity
484* High entry threshold
485* “Layers hell”
486* Typical problems of feature-based approaches
487
488# Partial Application
489
490> How to partially apply the methodology? Does it make sense? What if I ignore it?
491
492# Abstractions
493
494## The law of leaky abstractions
495
496[Section titled “The law of leaky abstractions”](#the-law-of-leaky-abstractions)
497
498## Why are there so many abstractions
499
500[Section titled “Why are there so many abstractions”](#why-are-there-so-many-abstractions)
501
502> 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
503
504> Architecture and applications in general are inherently complex, and the only question is how to better distribute and describe this complexity
505
506## About scopes of responsibility
507
508[Section titled “About scopes of responsibility”](#about-scopes-of-responsibility)
509
510> About optional abstractions
511
512## See also
513
514[Section titled “See also”](#see-also)
515
516* [About the need for new layers](https://t.me/feature_sliced/2801)
517* [About the difficulty in understanding the methodology and layers](https://t.me/feature_sliced/2619)
518
519# About architecture
520
521## Problems
522
523[Section titled “Problems”](#problems)
524
525Usually, the conversation about architecture is raised when the development stops due to certain problems in the project.
526
527### Bus-factor & Onboarding
528
529[Section titled “Bus-factor & Onboarding”](#bus-factor--onboarding)
530
531Only a limited number of people understand the project and its architecture
532
533**Examples:**
534
535* *“It’s difficult to add a person to the development”*
536* *“For every problem, everyone has their own opinion on how to get around” (let’s envy the angular)*
537* *“I don’t understand what is happening in this big piece of monolith”*
538
539### Implicit and uncontrolled consequences
540
541[Section titled “Implicit and uncontrolled consequences”](#implicit-and-uncontrolled-consequences)
542
543A lot of implicit side effects during development/refactoring *(“everything depends on everything”)*
544
545**Examples:**
546
547* *“The feature imports the feature”*
548* *“I updated the store of one page, and the functionality fell off on the other”*
549* *“The logic is smeared all over the application, and it is impossible to track where the beginning is, where the end is”*
550
551### Uncontrolled reuse of logic
552
553[Section titled “Uncontrolled reuse of logic”](#uncontrolled-reuse-of-logic)
554
555It is difficult to reuse/modify existing logic
556
557At the same time, there are usually [two extremes](https://github.com/feature-sliced/documentation/discussions/14):
558
559* Either the logic is written completely from scratch for each module *(with possible repetitions in the existing codebase)*
560* 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)*
561
562**Examples:**
563
564* *“I have **N** implementations of the same business logic in my project, for which I still pay”*
565* *“There are 6 different components of the button/pop-up/… In the project”*
566* *“Dump of helpers”*
567
568## Requirements
569
570[Section titled “Requirements”](#requirements)
571
572Therefore, it seems logical to present the desired *requirements for an ideal architecture:*
573
574Note
575
576Wherever it says “easy”, it means “relatively easy for a wide range of developers”, because it is clear that [it will not be possible to make an ideal solution for absolutely everyone](/docs/about/mission#limitations)
577
578### Explicitness
579
580[Section titled “Explicitness”](#explicitness)
581
582* It should be **easy to master and explain** the project and its architecture to the team
583* The structure should reflect the real **business values of the project**
584* There must be explicit **side effects and connections** between abstractions
585* It should be **easy to detect duplicate logic** without interfering with unique implementations
586* There should be no **dispersion of logic** throughout the project
587* There should not be **too many heterogeneous abstractions and rules** for a good architecture
588
589### Control
590
591[Section titled “Control”](#control)
592
593* A good architecture should **speed up the solution of tasks, the introduction of features**
594
595* It should be possible to control the development of the project
596
597* It should be easy to **expand, modify, delete the code**
598
599* The \* decomposition and isolation of\*\* functionality must be observed
600
601* Each component of the system must be **easily replaceable and removable**
602
603 * *[No need to optimize for changes](https://youtu.be/BWAeYuWFHhs?t=1631) - we can’t predict the future*
604 * *[Better-optimize for deletion](https://youtu.be/BWAeYuWFHhs?t=1666) - based on the context that already exists*
605
606### Adaptability
607
608[Section titled “Adaptability”](#adaptability)
609
610* A good architecture should be applicable **to most projects**
611
612 * *With existing infrastructure solutions*
613 * *At any stage of development*
614
615* There should be no dependence on the framework and platform
616
617* It should be possible to **easily scale the project and the team**, with the possibility of parallelization of development
618
619* It should be easy **to adapt to changing requirements and circumstances**
620
621## See also
622
623[Section titled “See also”](#see-also)
624
625* [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs)
626* [(React SPB Meetup #1) Sergey Sova - Feature Slices](https://t.me/feature_slices)
627* [(Article) About project modularization](https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1)
628* [(Article) About Separation of Concerns and structuring by features](https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/)
629
630# Knowledge types in the project
631
632The following “types of knowledge” can be distinguished in any project:
633
634* **Fundamental knowledge**\
635 Knowledge that does not change much over time, such as algorithms, computer science, programming language mechanisms and its APIs.
636
637* **Technology stack**\
638 Knowledge of the set of technical solutions used in a project, including programming languages, frameworks, and libraries.
639
640* **Project knowledge**\
641 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.
642
643Note
644
645**Feature-Sliced Design** is designed to reduce reliance on “project knowledge”, take more responsibility, and make it easier to onboard new team members.
646
647## See also
648
649[Section titled “See also”](#see-also)
650
651* [(Video 🇷🇺) Ilya Klimov - On Types of Knowledge](https://youtu.be/4xyb_tA-uw0?t=249)
652
653# Naming
654
655Different developers have different experiences and contexts, which can lead to misunderstandings on the team when the same entities are called differently. For example:
656
657* Components for display can be called “ui”, “components”, “ui-kit”, “views”, …
658* The code that is reused throughout the application can be called “core”, “shared”, “app”, …
659* Business logic code can be called “store”, “model”, “state”, …
660
661## Naming in Feature-Sliced Design
662
663[Section titled “Naming in Feature-Sliced Design”](#naming-in-fsd)
664
665The methodology uses specific terms such as:
666
667* “app”, “process”, “page”, “feature”, “entity”, “shared” as layer names,
668* “ui’, “model”, “lib”, “api”, “config” as segment names.
669
670It 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.
671
672## Naming Conflicts
673
674[Section titled “Naming Conflicts”](#when-can-naming-interfere)
675
676Naming conflicts can occur when terms used in the FSD methodology overlap with terms used in the business:
677
678* `FSD#process` vs simulated process in an application,
679* `FSD#page` vs log page,
680* `FSD#model` vs car model.
681
682For 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**.
683
684When 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.
685
686To 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.”
687
688Conversely, 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.
689
690## See also
691
692[Section titled “See also”](#see-also)
693
694* [(Discussion) Adaptability of naming](https://github.com/feature-sliced/documentation/discussions/16)
695* [(Discussion) Entity Naming Survey](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-464894)
696* [(Discussion) “processes” vs “flows” vs …](https://github.com/feature-sliced/documentation/discussions/20)
697* [(Discussion) “model” vs “store” vs …](https://github.com/feature-sliced/documentation/discussions/68)
698
699# Needs driven
700
701TL;DR
702
703:::note TL;DR
704
705— *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***
706
707— *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.***
708
709## Why?
710
711[Section titled “Why?”](#why)
712
713To 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.**
714
715> *@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.*
716
717*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.*
718
719## What tasks are we talking about?
720
721[Section titled “What tasks are we talking about?”](#what-tasks-are-we-talking-about)
722
723Frontend develops applications and interfaces for end users, so we solve the tasks of these consumers.
724
725When a person comes to us, **he wants to solve some of his pain or close a need.**
726
727*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).*
728
729**This very goal, with which the user came, is the task of the developers.**
730
731> *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.*
732
733## How does this affect development?
734
735[Section titled “How does this affect development?”](#how-does-this-affect-development)
736
737### Task decomposition
738
739[Section titled “Task decomposition”](#task-decomposition)
740
741When a developer begins to implement a task, in order to simplify the understanding and support of the code, he mentally **cuts it into stages**:
742
743* first *split into top-level entities* and *implement them*,
744* then these entities *split into smaller ones*
745* and so on
746
747*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*
748
749### Understanding the essence of the task
750
751[Section titled “Understanding the essence of the task”](#understanding-the-essence-of-the-task)
752
753But to give a clear name to an entity, **the developer must know enough about its purpose**
754
755* how is he going to use this entity,
756* what part of the user’s task does it implement, where else can this entity be applied,
757* in what other tasks can it participate,
758* and so on
759
760It 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.**
761
762> 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?
763
764## How to formulate it?
765
766[Section titled “How to formulate it?”](#how-to-formulate-it)
767
768**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.
769
770*The methodology can only tell the developer what tasks the product manager should pay close attention to.*
771
772> *@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.*
773>
774> *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.*
775
776As 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**
777
778Any 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.
779
780***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.*
781
782## And what is the benefit?
783
784[Section titled “And what is the benefit?”](#and-what-is-the-benefit)
785
786Now let’s look at the whole process from beginning to end.
787
788### 1. Understanding user tasks
789
790[Section titled “1. Understanding user tasks”](#1-understanding-user-tasks)
791
792When 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.
793
794> 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?*
795
796### 2. Structuring and ordering
797
798[Section titled “2. Structuring and ordering”](#2-structuring-and-ordering)
799
800With the understanding of tasks comes **a clear structure both in the head and in the tasks along with the code**
801
802### 3. Understanding the feature and its components
803
804[Section titled “3. Understanding the feature and its components”](#3-understanding-the-feature-and-its-components)
805
806**One feature is one useful functionality for the user**
807
808* When several features are implemented in one feature, this is **a violation of borders**
809* The feature can be indivisible and growing - **and this is not bad**
810* **Bad** - when the feature does not answer the question *“What is the business value for the user?”*
811* There can be no “map-office” feature
812 * But `booking-meeting-on-the-map`, `search-for-an-employee`, `change-of-workplace` - **yes**
813
814> *@sergeysova: The point is that the feature contains only code that implements the functionality itself*, without unnecessary details and internal solutions (ideally)\*
815>
816> *Open the feature code **and see only what relates to the task** - no more*
817
818### 4. Profit
819
820[Section titled “4. Profit”](#4-profit)
821
822Business 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.**
823
824*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.***
825
826> What is called [“Business Language” in Domain Driven Development](https://thedomaindrivendesign.io/developing-the-ubiquitous-language)
827
828***
829
830## Back to reality
831
832[Section titled “Back to reality”](#back-to-reality)
833
834If 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.*
835
836**However, in practice**, tasks and functionality are usually developed “too” iteratively and (or) there is no time to think through the design.
837
838**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.**
839
840> *\[[From the discussion](https://t.me/sergeysova/318)]: The developer tries to think 2-3 steps ahead, taking into account future wishes, but here he rests on his own experience*
841>
842> *Burns experience engineer usually immediately looking 10 steps ahead, and understand where one feature to divide and combine with the other*
843>
844> *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*
845
846## The role of methodology
847
848[Section titled “The role of methodology”](#the-role-of-methodology)
849
850**The methodology helps to solve the problems of developers, so that it is easier to solve the problems of users.**
851
852There is no solution to the problems of developers only for the sake of developers
853
854But in order for the developer to solve his tasks, **you need to understand the user’s tasks** - on the contrary, it will not work
855
856### Methodology requirements
857
858[Section titled “Methodology requirements”](#methodology-requirements)
859
860It becomes clear that you need to identify at least two requirements for **Feature-Sliced Design**:
861
8621. The methodology should tell **how to create features, processes and entities**
863
864 * 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.
865
8662. The methodology should help the architecture **[easily adapt to the changing requirements of the project](/docs/about/understanding/architecture#adaptability)**
867
868## See also
869
870[Section titled “See also”](#see-also)
871
872* [(Post) Stimulation for a clear formulation of tasks (+ discussion)](https://t.me/sergeysova/318)
873
874 > ***The current article** is an adaptation of this discussion, you can read the full uncut version at the link*
875
876* [(Discussion) How to break the functionality and what it is](https://t.me/atomicdesign/18972)
877
878* [(Article) “How to better organize your applications”](https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1)
879
880# Signals of architecture
881
882> If there is a limitation on the part of the architecture, then there are obvious reasons for this, and consequences if they are ignored
883
884> 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)
885
886## See also
887
888[Section titled “See also”](#see-also)
889
890* [(Thread) About signals from architecture and dataflow](https://t.me/feature_sliced/2070)
891* [(Thread) About the fundamental nature of architecture](https://t.me/feature_sliced/2492)
892* [(Thread) About highlighting weak points](https://t.me/feature_sliced/3979)
893* [(Thread) How to understand that the data model is swollen](https://t.me/feature_sliced/4228)
894
895# Branding Guidelines
896
897FSD’s visual identity is based on its core-concepts: `Layered`, `Sliced self-contained parts`, `Parts & Compose`, `Segmented`. But also we tend to design simple, pretty identity, which should convey the FSD philisophy and be easy to recognize.
898
899**Please, use FSD’s identity “as-is”, without changes but with our assets for your comfort.** This brand guide will help you to use FSD’s identity correctly.
900
901Caution
902
903FSD had [another legacy identity](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing) before. Old design didn’t represent core-concepts of methodology. Also it was created as pure draft, and should have been actualized.
904
905For a compatible and long-term use of the brand, we have been carefully rebranding for a year (2021-2022). **So that you can be sure when using identity of FSD 🍰**
906
907*But prefer namely actual identity, not old!*
908
909## Title
910
911[Section titled “Title”](#title)
912
913* ✅ **Correct:** `Feature-Sliced Design`, `FSD`
914* ❌ **Incorrect:** `Feature-Sliced`, `Feature Sliced`, `FeatureSliced`, `feature-sliced`, `feature sliced`, `FS`
915
916## Emojii
917
918[Section titled “Emojii”](#emojii)
919
920The cake 🍰 image represents FSD core concepts quite well, so it has been chosen as our signature emoji
921
922> Example: *”🍰 Architectural design methodology for Frontend projects”*
923
924## Logo & Palette
925
926[Section titled “Logo & Palette”](#logo--palette)
927
928FSD has few variations of logo for different context, but it recommended to prefer **primary**
929
930| | | |
931| -------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------- |
932| Theme | Logo (Ctrl/Cmd + Click for download) | Usage |
933| primary (#29BEDC, #517AED) | [](/_astro/logo-primary.CDj-FIdD.png) | Preferred in most cases |
934| flat (#3193FF) | [](/_astro/logo-flat.DtIserW_.png) | For one-color context |
935| monochrome (#FFF) | [](/_astro/logo-monochrome.0p3AxdYi.png) | For grayscale context |
936| square (#3193FF) | [](/_astro/logo-square.BjrpqCvQ.png) | For square boundaries |
937
938## Banners & Schemes
939
940[Section titled “Banners & Schemes”](#banners--schemes)
941
942[ ](/_astro/banner-primary.CctEYme2.jpg)[](/_astro/banner-monochrome.DFz3Ww19.jpg)
943
944## Social Preview
945
946[Section titled “Social Preview”](#social-preview)
947
948Work in progress…
949
950## Presentation template
951
952[Section titled “Presentation template”](#presentation-template)
953
954Work in progress…
955
956## See also
957
958[Section titled “See also”](#see-also)
959
960* [Discussion (github)](https://github.com/feature-sliced/documentation/discussions/399)
961* [History of development with references (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1)
962
963# FAQ
964
965Note
966
967You can ask your question in our [Telegram chat](https://t.me/feature_sliced), [Discord community](https://discord.gg/S8MzWTUsmp), and [GitHub Discussions](https://github.com/feature-sliced/documentation/discussions).
968
969### Is there a toolkit or a linter?
970
971[Section titled “Is there a toolkit or a linter?”](#is-there-a-toolkit-or-a-linter)
972
973Yes! We have a linter called [Steiger](https://github.com/feature-sliced/steiger) to check your project’s architecture and [folder generators](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) through a CLI or IDEs.
974
975### Where to store the layout/template of pages?
976
977[Section titled “Where to store the layout/template of pages?”](#where-to-store-the-layouttemplate-of-pages)
978
979If 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:
980
981* 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.
982* 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.
983
984### What is the difference between a feature and an entity?
985
986[Section titled “What is the difference between a feature and an entity?”](#what-is-the-difference-between-a-feature-and-an-entity)
987
988An *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.
989
990For more information, along with examples, see the Reference page on [slices](/docs/reference/layers#entities).
991
992### Can I embed pages/features/entities into each other?
993
994[Section titled “Can I embed pages/features/entities into each other?”](#can-i-embed-pagesfeaturesentities-into-each-other)
995
996Yes, 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.
997
998You cannot import one feature from another feature, this is prohibited by the [**import rule on layers**](/docs/reference/layers#import-rule-on-layers).
999
1000### What about Atomic Design?
1001
1002[Section titled “What about Atomic Design?”](#what-about-atomic-design)
1003
1004The current version of the methodology does not require nor prohibit the use of Atomic Design together with Feature-Sliced Design.
1005
1006For example, Atomic Design [can be applied well](https://t.me/feature_sliced/1653) for the `ui` segment of modules.
1007
1008### Are there any useful resources/articles/etc. about FSD?
1009
1010[Section titled “Are there any useful resources/articles/etc. about FSD?”](#are-there-any-useful-resourcesarticlesetc-about-fsd)
1011
1012Yes! <https://github.com/feature-sliced/awesome>
1013
1014### Why do I need Feature-Sliced Design?
1015
1016[Section titled “Why do I need Feature-Sliced Design?”](#why-do-i-need-feature-sliced-design)
1017
1018It 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 [motivation](/docs/about/motivation) page to learn more about why FSD was created.
1019
1020### Does a novice developer need an architecture/methodology?
1021
1022[Section titled “Does a novice developer need an architecture/methodology?”](#does-a-novice-developer-need-an-architecturemethodology)
1023
1024Rather yes than no
1025
1026*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*
1027
1028### How do I work with the authorization context?
1029
1030[Section titled “How do I work with the authorization context?”](#how-do-i-work-with-the-authorization-context)
1031
1032Answered [here](/docs/guides/examples/auth)
1033
1034# Overview
1035
1036**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.
1037
1038Apart from a set of conventions, FSD is also a toolchain. We have a [linter](https://github.com/feature-sliced/steiger) to check your project’s architecture, [folder generators](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) through a CLI or IDEs, as well as a rich library of examples.
1039
1040## Is it right for me?
1041
1042[Section titled “Is it right for me?”](#is-it-right-for-me)
1043
1044FSD can be implemented in projects and teams of any size. It is right for your project if:
1045
1046* You’re doing **frontend** (UI on web, mobile, desktop, etc.)
1047* You’re building an **application**, not a library
1048
1049And 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.
1050
1051If 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 [Migration](/docs/guides/migration/from-custom) section for guidance.
1052
1053## Basic example
1054
1055[Section titled “Basic example”](#basic-example)
1056
1057Here is a simple project that implements FSD:
1058
1059* app/
1060
1061 * …
1062
1063* pages/
1064
1065 * …
1066
1067* shared/
1068
1069 * …
1070
1071These top-level folders are called *layers*. Let’s look deeper:
1072
1073* app/
1074
1075 * routes/
1076
1077 * …
1078
1079 * analytics/
1080
1081 * …
1082
1083* pages/
1084
1085 * home/
1086
1087 * …
1088
1089 * article-reader/
1090
1091 * ui/
1092
1093 * …
1094
1095 * api/
1096
1097 * …
1098
1099 * setings/
1100
1101 * …
1102
1103* shared/
1104
1105 * ui/
1106
1107 * …
1108
1109 * api/
1110
1111 * …
1112
1113Folders inside `📂 pages` are called *slices*. They divide the layer by domain (in this case, by pages).
1114
1115Folders 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.
1116
1117## Concepts
1118
1119[Section titled “Concepts”](#concepts)
1120
1121Layers, slices, and segments form a hierarchy like this:
1122
1123
1124
1125Pictured above: three pillars, labeled left to right as “Layers”, “Slices”, and “Segments” respectively.
1126
1127The “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”.
1128
1129The “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”.
1130
1131The “Segments” pillar contains three divisions, arranged top to bottom and labeled “ui”, “model”, and “api”.
1132
1133### Layers
1134
1135[Section titled “Layers”](#layers)
1136
1137Layers 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):
1138
11391. **App** — everything that makes the app run — routing, entrypoints, global styles, providers.
11402. **Processes** (deprecated) — complex inter-page scenarios.
11413. **Pages** — full pages or large parts of a page in nested routing.
11424. **Widgets** — large self-contained chunks of functionality or UI, usually delivering an entire use case.
11435. **Features** — *reused* implementations of entire product features, i.e. actions that bring business value to the user.
11446. **Entities** — business entities that the project works with, like `user` or `product`.
11457. **Shared** — reusable functionality, especially when it’s detached from the specifics of the project/business, though not necessarily.
1146
1147Caution
1148
1149Layers **App** and **Shared**, unlike other layers, do not have slices and are divided into segments directly.
1150
1151However, all other layers — **Entities**, **Features**, **Widgets**, and **Pages**, retain the structure in which you must first create slices, inside which you create the segments.
1152
1153The trick with layers is that modules on one layer can only know about and import from modules from the layers strictly below.
1154
1155### Slices
1156
1157[Section titled “Slices”](#slices)
1158
1159Next 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.
1160
1161Slices cannot use other slices on the same layer, and that helps with high cohesion and low coupling.
1162
1163### Segments
1164
1165[Section titled “Segments”](#segments)
1166
1167Slices, 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:
1168
1169* `ui` — everything related to UI display: UI components, date formatters, styles, etc.
1170* `api` — backend interactions: request functions, data types, mappers, etc.
1171* `model` — the data model: schemas, interfaces, stores, and business logic.
1172* `lib` — library code that other modules on this slice need.
1173* `config` — configuration files and feature flags.
1174
1175Usually these segments are enough for most layers, you would only create your own segments in Shared or App, but this is not a rule.
1176
1177## Advantages
1178
1179[Section titled “Advantages”](#advantages)
1180
1181* **Uniformity** Since the structure is standardized, projects become more uniform, which makes onboarding new members easier for the team.
1182
1183* **Stability in face of changes and refactoring** A module on one layer cannot use other modules on the same layer, or the layers above. This allows you to make isolated modifications without unforeseen consequences to the rest of the app.
1184
1185* **Controlled reuse of logic** Depending on the layer, you can make code very reusable or very local. This keeps a balance between following the **DRY** principle and practicality.
1186
1187* **Orientation to business and users needs** The app is split into business domains and usage of the business language is encouraged in naming, so that you can do useful product work without fully understanding all other unrelated parts of the project.
1188
1189## Incremental adoption
1190
1191[Section titled “Incremental adoption”](#incremental-adoption)
1192
1193If 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.
1194
11951. Start by slowly shaping up the App and Shared layers module-by-module to create a foundation.
1196
11972. Distribute all of the existing UI across Widgets and Pages using broad strokes, even if they have dependencies that violate the rules of FSD.
1198
11993. Start gradually resolving import violations and also extracting Entities and possibly even Features.
1200
1201It’s advised to refrain from adding new large entities while refactoring or refactoring only certain parts of the project.
1202
1203## Next steps
1204
1205[Section titled “Next steps”](#next-steps)
1206
1207[Want to get a good grasp of how to think in FSD? ](/docs/get-started/tutorial)Check out the Tutorial
1208
1209[Have questions? ](https://t.me/feature_sliced)Drop by our Telegram chat and get help from the community
1210
1211# Tutorial
1212
1213## Part 1. On paper
1214
1215[Section titled “Part 1. On paper”](#part-1-on-paper)
1216
1217This tutorial will examine the Real World App, also known as Conduit. Conduit is a basic [Medium](https://medium.com/) clone — it lets you read and write articles as well as comment on the articles of others.
1218
1219
1220
1221This 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?
1222
1223### Start by listing the pages
1224
1225[Section titled “Start by listing the pages”](#start-by-listing-the-pages)
1226
1227If we look at the screenshot above, we can assume at least the following pages:
1228
1229* Home (article feed)
1230* Sign in and sign up
1231* Article reader
1232* Article editor
1233* User profile viewer
1234* User profile editor (user settings)
1235
1236Every 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`.
1237
1238As such, our Pages folder will look like this:
1239
1240* pages/
1241
1242 * feed/
1243
1244 * …
1245
1246 * sign-in/
1247
1248 * …
1249
1250 * article-read/
1251
1252 * …
1253
1254 * article-edit/
1255
1256 * …
1257
1258 * profile/
1259
1260 * …
1261
1262 * settings/
1263
1264 * …
1265
1266The 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**:
1267
1268*A module (file) in a slice can only import other slices when they are located on layers strictly below.*
1269
1270In 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.
1271
1272### Close look at the feed
1273
1274[Section titled “Close look at the feed”](#close-look-at-the-feed)
1275
1276
1277
1278*Anonymous user’s perspective*
1279
1280
1281
1282*Authenticated user’s perspective*
1283
1284There are three dynamic areas on the feed page:
1285
12861. Sign-in links with an indication if you are signed in
12872. List of tags that triggers filtering in the feed
12883. One/two feeds of articles, each article with a like button
1289
1290The sign-in links are a part of a header that is common to all pages, we will revisit it separately.
1291
1292#### List of tags
1293
1294[Section titled “List of tags”](#list-of-tags)
1295
1296To 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:
1297
1298* 📂 `api/` for backend interactions
1299* 📂 `ui/` for code that handles rendering and appearance
1300* 📂 `model/` for storage and business logic
1301* 📂 `config/` for feature flags, environment variables and other forms of configuration
1302
1303We will place code that fetches tags into `api`, the tag component into `ui`, and the storage interaction into `model`.
1304
1305#### Articles
1306
1307[Section titled “Articles”](#articles)
1308
1309Using the same grouping principles, we can decompose the feed of articles into the same three segments:
1310
1311* 📂 `api/`: fetch paginated articles with like count; like an article
1312
1313* 📂 `ui/`:
1314
1315 * tab list that can render an extra tab if a tag is selected
1316 * individual article
1317 * functional pagination
1318
1319* 📂 `model/`: client-side storage of the currently loaded articles and current page (if needed)
1320
1321### Reuse generic code
1322
1323[Section titled “Reuse generic code”](#reuse-generic-code)
1324
1325Most 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**.
1326
1327Shared 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.
1328
1329Usually, 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:
1330
1331* 📂 `ui/` — the UI kit, pure appearance, no business logic. For example, buttons, modal dialogs, form inputs.
1332* 📂 `api/` — convenience wrappers around request making primitives (like `fetch()` on the Web) and, optionally, functions for triggering particular requests according to the backend specification.
1333* 📂 `config/` — parsing environment variables
1334* 📂 `i18n/` — configuration of language support
1335* 📂 `router/` — routing primitives and route constants
1336
1337Those 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.
1338
1339### Define a strict public API
1340
1341[Section titled “Define a strict public API”](#define-a-strict-public-api)
1342
1343In 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.
1344
1345For 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.
1346
1347Our slices/segments will appear to each other as follows:
1348
1349* pages/
1350
1351 * feed/
1352
1353 * index
1354
1355 * sign-in/
1356
1357 * index
1358
1359 * article-read/
1360
1361 * index
1362
1363 * …
1364
1365* shared/
1366
1367 * ui/
1368
1369 * index
1370
1371 * api/
1372
1373 * index
1374
1375 * …
1376
1377Whatever 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.
1378
1379### Large reused blocks in the UI
1380
1381[Section titled “Large reused blocks in the UI”](#large-reused-blocks-in-the-ui)
1382
1383Earlier 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.
1384
1385Between 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.
1386
1387In 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.
1388
1389### Close look at a page with a form
1390
1391[Section titled “Close look at a page with a form”](#close-look-at-a-page-with-a-form)
1392
1393Let’s also examine a page that’s intended for editing, not reading. For example, the article writer:
1394
1395
1396
1397It looks trivial, but contains several aspects of application development that we haven’t explored yet — form validation, error states, and data persistence.
1398
1399If 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.
1400
1401To 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.
1402
1403To improve user experience, we could also persist the inputs to prevent accidental data loss. This is also a job of the `model` segment.
1404
1405### Summary
1406
1407[Section titled “Summary”](#summary)
1408
1409We have examined several pages and outlined a preliminary structure for our application:
1410
14111. Shared layer
1412
1413 1. `ui` will contain our reusable UI kit
1414 2. `api` will contain our primitive interactions with the backend
1415 3. The rest will be arranged on demand
1416
14172. Pages layer — each page is a separate slice
1418
1419 1. `ui` will contain the page itself and all of its parts
1420 2. `api` will contain more specialized data fetching, using `shared/api`
1421 3. `model` might contain client-side storage of the data that we will display
1422
1423Let’s get building!
1424
1425## Part 2. In code
1426
1427[Section titled “Part 2. In code”](#part-2-in-code)
1428
1429Now that we have a plan, let’s put it to practice. We will use React and [Remix](https://remix.run).
1430
1431There’s a template ready for this project, clone it from GitHub to get a headstart: <https://github.com/feature-sliced/tutorial-conduit/tree/clean>.
1432
1433Install dependencies with `npm install` and start the development server with `npm run dev`. Open `http://localhost:3000` and you should see a blank app.
1434
1435### Lay out the pages
1436
1437[Section titled “Lay out the pages”](#lay-out-the-pages)
1438
1439Let’s start by creating blank components for all our pages. Run the following command in your project:
1440
1441```bash
1442npx fsd pages feed sign-in article-read article-edit profile settings --segments ui
1443```
1444
1445This will create folders like `pages/feed/ui/` and an index file, `pages/feed/index.ts`, for every page.
1446
1447### Connect the feed page
1448
1449[Section titled “Connect the feed page”](#connect-the-feed-page)
1450
1451Let’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:
1452
1453pages/feed/ui/FeedPage.tsx
1454
1455```tsx
1456export function FeedPage() {
1457 return (
1458 <div className="home-page">
1459 <div className="banner">
1460 <div className="container">
1461 <h1 className="logo-font">conduit</h1>
1462 <p>A place to share your knowledge.</p>
1463 </div>
1464 </div>
1465 </div>
1466 );
1467}
1468```
1469
1470Then re-export this component in the feed page’s public API, the `pages/feed/index.ts` file:
1471
1472pages/feed/index.ts
1473
1474```ts
1475export { FeedPage } from "./ui/FeedPage";
1476```
1477
1478Now 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.
1479
1480Use the `FeedPage` component in `app/routes/_index.tsx`:
1481
1482app/routes/\_index.tsx
1483
1484```tsx
1485import type { MetaFunction } from "@remix-run/node";
1486import { FeedPage } from "pages/feed";
1487
1488
1489export const meta: MetaFunction = () => {
1490 return [{ title: "Conduit" }];
1491};
1492
1493
1494export default FeedPage;
1495```
1496
1497Then, if you run the dev server and open the application, you should see the Conduit banner!
1498
1499
1500
1501### API client
1502
1503[Section titled “API client”](#api-client)
1504
1505To 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:
1506
1507```bash
1508npx fsd shared --segments api config
1509```
1510
1511Then create `shared/config/backend.ts`:
1512
1513shared/config/backend.ts
1514
1515```tsx
1516export { mockBackendUrl as backendBaseUrl } from "mocks/handlers";
1517```
1518
1519shared/config/index.ts
1520
1521```tsx
1522export { backendBaseUrl } from "./backend";
1523```
1524
1525Since the RealWorld project conveniently provides an [OpenAPI specification](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml), we can take advantage of auto-generated types for our client. We will use [the `openapi-fetch` package](https://openapi-ts.pages.dev/openapi-fetch/) that comes with an additional type generator.
1526
1527Run the following command to generate up-to-date API typings:
1528
1529```bash
1530npm run generate-api-types
1531```
1532
1533This 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`:
1534
1535shared/api/client.ts
1536
1537```tsx
1538import createClient from "openapi-fetch";
1539
1540
1541import { backendBaseUrl } from "shared/config";
1542import type { paths } from "./v1";
1543
1544
1545export const { GET, POST, PUT, DELETE } = createClient<paths>({ baseUrl: backendBaseUrl });
1546```
1547
1548shared/api/index.ts
1549
1550```tsx
1551export { GET, POST, PUT, DELETE } from "./client";
1552```
1553
1554### Real data in the feed
1555
1556[Section titled “Real data in the feed”](#real-data-in-the-feed)
1557
1558We can now proceed to adding articles to the feed, fetched from the backend. Let’s begin by implementing an article preview component.
1559
1560Create `pages/feed/ui/ArticlePreview.tsx` with the following content:
1561
1562pages/feed/ui/ArticlePreview\.tsx
1563
1564```tsx
1565export function ArticlePreview({ article }) { /* TODO */ }
1566```
1567
1568Since 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:
1569
1570shared/api/models.ts
1571
1572```tsx
1573import type { components } from "./v1";
1574
1575
1576export type Article = components["schemas"]["Article"];
1577```
1578
1579shared/api/index.ts
1580
1581```tsx
1582export { GET, POST, PUT, DELETE } from "./client";
1583
1584
1585export type { Article } from "./models";
1586```
1587
1588Now we can come back to the article preview component and fill the markup with data. Update the component with the following content:
1589
1590pages/feed/ui/ArticlePreview\.tsx
1591
1592```tsx
1593import { Link } from "@remix-run/react";
1594import type { Article } from "shared/api";
1595
1596
1597interface ArticlePreviewProps {
1598 article: Article;
1599}
1600
1601
1602export function ArticlePreview({ article }: ArticlePreviewProps) {
1603 return (
1604 <div className="article-preview">
1605 <div className="article-meta">
1606 <Link to={`/profile/${article.author.username}`} prefetch="intent">
1607 <img src={article.author.image} alt="" />
1608 </Link>
1609 <div className="info">
1610 <Link
1611 to={`/profile/${article.author.username}`}
1612 className="author"
1613 prefetch="intent"
1614 >
1615 {article.author.username}
1616 </Link>
1617 <span className="date" suppressHydrationWarning>
1618 {new Date(article.createdAt).toLocaleDateString(undefined, {
1619 dateStyle: "long",
1620 })}
1621 </span>
1622 </div>
1623 <button className="btn btn-outline-primary btn-sm pull-xs-right">
1624 <i className="ion-heart"></i> {article.favoritesCount}
1625 </button>
1626 </div>
1627 <Link
1628 to={`/article/${article.slug}`}
1629 className="preview-link"
1630 prefetch="intent"
1631 >
1632 <h1>{article.title}</h1>
1633 <p>{article.description}</p>
1634 <span>Read more...</span>
1635 <ul className="tag-list">
1636 {article.tagList.map((tag) => (
1637 <li key={tag} className="tag-default tag-pill tag-outline">
1638 {tag}
1639 </li>
1640 ))}
1641 </ul>
1642 </Link>
1643 </div>
1644 );
1645}
1646```
1647
1648The 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.
1649
1650Now 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:
1651
1652pages/feed/api/loader.ts
1653
1654```tsx
1655import { json } from "@remix-run/node";
1656
1657
1658import { GET } from "shared/api";
1659
1660
1661export const loader = async () => {
1662 const { data: articles, error, response } = await GET("/articles");
1663
1664
1665 if (error !== undefined) {
1666 throw json(error, { status: response.status });
1667 }
1668
1669
1670 return json({ articles });
1671};
1672```
1673
1674To connect it to the page, we need to export it with the name `loader` from the route file:
1675
1676pages/feed/index.ts
1677
1678```tsx
1679export { FeedPage } from "./ui/FeedPage";
1680export { loader } from "./api/loader";
1681```
1682
1683app/routes/\_index.tsx
1684
1685```tsx
1686import type { MetaFunction } from "@remix-run/node";
1687import { FeedPage } from "pages/feed";
1688
1689
1690export { loader } from "pages/feed";
1691
1692
1693export const meta: MetaFunction = () => {
1694 return [{ title: "Conduit" }];
1695};
1696
1697
1698export default FeedPage;
1699```
1700
1701And the final step is to render these cards in the feed. Update your `FeedPage` with the following code:
1702
1703pages/feed/ui/FeedPage.tsx
1704
1705```tsx
1706import { useLoaderData } from "@remix-run/react";
1707
1708
1709import type { loader } from "../api/loader";
1710import { ArticlePreview } from "./ArticlePreview";
1711
1712
1713export function FeedPage() {
1714 const { articles } = useLoaderData<typeof loader>();
1715
1716
1717 return (
1718 <div className="home-page">
1719 <div className="banner">
1720 <div className="container">
1721 <h1 className="logo-font">conduit</h1>
1722 <p>A place to share your knowledge.</p>
1723 </div>
1724 </div>
1725
1726
1727 <div className="container page">
1728 <div className="row">
1729 <div className="col-md-9">
1730 {articles.articles.map((article) => (
1731 <ArticlePreview key={article.slug} article={article} />
1732 ))}
1733 </div>
1734 </div>
1735 </div>
1736 </div>
1737 );
1738}
1739```
1740
1741### Filtering by tag
1742
1743[Section titled “Filtering by tag”](#filtering-by-tag)
1744
1745Regarding 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.
1746
1747Update the loader file, `pages/feed/api/loader.ts`, with the following code:
1748
1749pages/feed/api/loader.ts
1750
1751```tsx
1752import { json } from "@remix-run/node";
1753import type { FetchResponse } from "openapi-fetch";
1754import { promiseHash } from "remix-utils/promise";
1755
1756
1757import { GET } from "shared/api";
1758
1759
1760async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
1761 responsePromise: Promise<FetchResponse<T, O, Media>>,
1762) {
1763 const { data, error, response } = await responsePromise;
1764
1765
1766 if (error !== undefined) {
1767 throw json(error, { status: response.status });
1768 }
1769
1770
1771 return data as NonNullable<typeof data>;
1772}
1773
1774
1775export const loader = async () => {
1776 return json(
1777 await promiseHash({
1778 articles: throwAnyErrors(GET("/articles")),
1779 tags: throwAnyErrors(GET("/tags")),
1780 }),
1781 );
1782};
1783```
1784
1785You 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.
1786
1787Now, 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.
1788
1789Update `pages/feed/ui/FeedPage.tsx` with the following code:
1790
1791pages/feed/ui/FeedPage.tsx
1792
1793```tsx
1794import { Form, useLoaderData } from "@remix-run/react";
1795import { ExistingSearchParams } from "remix-utils/existing-search-params";
1796
1797
1798import type { loader } from "../api/loader";
1799import { ArticlePreview } from "./ArticlePreview";
1800
1801
1802export function FeedPage() {
1803 const { articles, tags } = useLoaderData<typeof loader>();
1804
1805
1806 return (
1807 <div className="home-page">
1808 <div className="banner">
1809 <div className="container">
1810 <h1 className="logo-font">conduit</h1>
1811 <p>A place to share your knowledge.</p>
1812 </div>
1813 </div>
1814
1815
1816 <div className="container page">
1817 <div className="row">
1818 <div className="col-md-9">
1819 {articles.articles.map((article) => (
1820 <ArticlePreview key={article.slug} article={article} />
1821 ))}
1822 </div>
1823
1824
1825 <div className="col-md-3">
1826 <div className="sidebar">
1827 <p>Popular Tags</p>
1828
1829
1830 <Form>
1831 <ExistingSearchParams exclude={["tag"]} />
1832 <div className="tag-list">
1833 {tags.tags.map((tag) => (
1834 <button
1835 key={tag}
1836 name="tag"
1837 value={tag}
1838 className="tag-pill tag-default"
1839 >
1840 {tag}
1841 </button>
1842 ))}
1843 </div>
1844 </Form>
1845 </div>
1846 </div>
1847 </div>
1848 </div>
1849 </div>
1850 );
1851}
1852```
1853
1854Then we need to use the `tag` search parameter in our loader. Change the `loader` function in `pages/feed/api/loader.ts` to the following:
1855
1856pages/feed/api/loader.ts
1857
1858```tsx
1859import { json, type LoaderFunctionArgs } from "@remix-run/node";
1860import type { FetchResponse } from "openapi-fetch";
1861import { promiseHash } from "remix-utils/promise";
1862
1863
1864import { GET } from "shared/api";
1865
1866
1867async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
1868 responsePromise: Promise<FetchResponse<T, O, Media>>,
1869) {
1870 const { data, error, response } = await responsePromise;
1871
1872
1873 if (error !== undefined) {
1874 throw json(error, { status: response.status });
1875 }
1876
1877
1878 return data as NonNullable<typeof data>;
1879}
1880
1881
1882export const loader = async ({ request }: LoaderFunctionArgs) => {
1883 const url = new URL(request.url);
1884 const selectedTag = url.searchParams.get("tag") ?? undefined;
1885
1886
1887 return json(
1888 await promiseHash({
1889 articles: throwAnyErrors(
1890 GET("/articles", { params: { query: { tag: selectedTag } } }),
1891 ),
1892 tags: throwAnyErrors(GET("/tags")),
1893 }),
1894 );
1895};
1896```
1897
1898That’s it, no `model` segment necessary. Remix is pretty neat.
1899
1900### Pagination
1901
1902[Section titled “Pagination”](#pagination)
1903
1904In 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.
1905
1906pages/feed/api/loader.ts
1907
1908```tsx
1909import { json, type LoaderFunctionArgs } from "@remix-run/node";
1910import type { FetchResponse } from "openapi-fetch";
1911import { promiseHash } from "remix-utils/promise";
1912
1913
1914import { GET } from "shared/api";
1915
1916
1917async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
1918 responsePromise: Promise<FetchResponse<T, O, Media>>,
1919) {
1920 const { data, error, response } = await responsePromise;
1921
1922
1923 if (error !== undefined) {
1924 throw json(error, { status: response.status });
1925 }
1926
1927
1928 return data as NonNullable<typeof data>;
1929}
1930
1931
1932/** Amount of articles on one page. */
1933export const LIMIT = 20;
1934
1935
1936export const loader = async ({ request }: LoaderFunctionArgs) => {
1937 const url = new URL(request.url);
1938 const selectedTag = url.searchParams.get("tag") ?? undefined;
1939 const page = parseInt(url.searchParams.get("page") ?? "", 10);
1940
1941
1942 return json(
1943 await promiseHash({
1944 articles: throwAnyErrors(
1945 GET("/articles", {
1946 params: {
1947 query: {
1948 tag: selectedTag,
1949 limit: LIMIT,
1950 offset: !Number.isNaN(page) ? page * LIMIT : undefined,
1951 },
1952 },
1953 }),
1954 ),
1955 tags: throwAnyErrors(GET("/tags")),
1956 }),
1957 );
1958};
1959```
1960
1961pages/feed/ui/FeedPage.tsx
1962
1963```tsx
1964import { Form, useLoaderData, useSearchParams } from "@remix-run/react";
1965import { ExistingSearchParams } from "remix-utils/existing-search-params";
1966
1967
1968import { LIMIT, type loader } from "../api/loader";
1969import { ArticlePreview } from "./ArticlePreview";
1970
1971
1972export function FeedPage() {
1973 const [searchParams] = useSearchParams();
1974 const { articles, tags } = useLoaderData<typeof loader>();
1975 const pageAmount = Math.ceil(articles.articlesCount / LIMIT);
1976 const currentPage = parseInt(searchParams.get("page") ?? "1", 10);
1977
1978
1979 return (
1980 <div className="home-page">
1981 <div className="banner">
1982 <div className="container">
1983 <h1 className="logo-font">conduit</h1>
1984 <p>A place to share your knowledge.</p>
1985 </div>
1986 </div>
1987
1988
1989 <div className="container page">
1990 <div className="row">
1991 <div className="col-md-9">
1992 {articles.articles.map((article) => (
1993 <ArticlePreview key={article.slug} article={article} />
1994 ))}
1995
1996
1997 <Form>
1998 <ExistingSearchParams exclude={["page"]} />
1999 <ul className="pagination">
2000 {Array(pageAmount)
2001 .fill(null)
2002 .map((_, index) =>
2003 index + 1 === currentPage ? (
2004 <li key={index} className="page-item active">
2005 <span className="page-link">{index + 1}</span>
2006 </li>
2007 ) : (
2008 <li key={index} className="page-item">
2009 <button
2010 className="page-link"
2011 name="page"
2012 value={index + 1}
2013 >
2014 {index + 1}
2015 </button>
2016 </li>
2017 ),
2018 )}
2019 </ul>
2020 </Form>
2021 </div>
2022
2023
2024 <div className="col-md-3">
2025 <div className="sidebar">
2026 <p>Popular Tags</p>
2027
2028
2029 <Form>
2030 <ExistingSearchParams exclude={["tag", "page"]} />
2031 <div className="tag-list">
2032 {tags.tags.map((tag) => (
2033 <button
2034 key={tag}
2035 name="tag"
2036 value={tag}
2037 className="tag-pill tag-default"
2038 >
2039 {tag}
2040 </button>
2041 ))}
2042 </div>
2043 </Form>
2044 </div>
2045 </div>
2046 </div>
2047 </div>
2048 </div>
2049 );
2050}
2051```
2052
2053So 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!
2054
2055### Authentication
2056
2057[Section titled “Authentication”](#authentication)
2058
2059Authentication 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.
2060
2061Create `RegisterPage.tsx` in the `ui` segment of `pages/sign-in` with the following content:
2062
2063pages/sign-in/ui/RegisterPage.tsx
2064
2065```tsx
2066import { Form, Link, useActionData } from "@remix-run/react";
2067
2068
2069import type { register } from "../api/register";
2070
2071
2072export function RegisterPage() {
2073 const registerData = useActionData<typeof register>();
2074
2075
2076 return (
2077 <div className="auth-page">
2078 <div className="container page">
2079 <div className="row">
2080 <div className="col-md-6 offset-md-3 col-xs-12">
2081 <h1 className="text-xs-center">Sign up</h1>
2082 <p className="text-xs-center">
2083 <Link to="/login">Have an account?</Link>
2084 </p>
2085
2086
2087 {registerData?.error && (
2088 <ul className="error-messages">
2089 {registerData.error.errors.body.map((error) => (
2090 <li key={error}>{error}</li>
2091 ))}
2092 </ul>
2093 )}
2094
2095
2096 <Form method="post">
2097 <fieldset className="form-group">
2098 <input
2099 className="form-control form-control-lg"
2100 type="text"
2101 name="username"
2102 placeholder="Username"
2103 />
2104 </fieldset>
2105 <fieldset className="form-group">
2106 <input
2107 className="form-control form-control-lg"
2108 type="text"
2109 name="email"
2110 placeholder="Email"
2111 />
2112 </fieldset>
2113 <fieldset className="form-group">
2114 <input
2115 className="form-control form-control-lg"
2116 type="password"
2117 name="password"
2118 placeholder="Password"
2119 />
2120 </fieldset>
2121 <button className="btn btn-lg btn-primary pull-xs-right">
2122 Sign up
2123 </button>
2124 </Form>
2125 </div>
2126 </div>
2127 </div>
2128 </div>
2129 );
2130}
2131```
2132
2133We have a broken import to fix now. It involves a new segment, so create that:
2134
2135```bash
2136npx fsd pages sign-in -s api
2137```
2138
2139However, 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.
2140
2141Put 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:
2142
2143shared/api/auth.server.ts
2144
2145```tsx
2146import { createCookieSessionStorage, redirect } from "@remix-run/node";
2147import invariant from "tiny-invariant";
2148
2149
2150import type { User } from "./models";
2151
2152
2153invariant(
2154 process.env.SESSION_SECRET,
2155 "SESSION_SECRET must be set for authentication to work",
2156);
2157
2158
2159const sessionStorage = createCookieSessionStorage<{
2160 user: User;
2161}>({
2162 cookie: {
2163 name: "__session",
2164 httpOnly: true,
2165 path: "/",
2166 sameSite: "lax",
2167 secrets: [process.env.SESSION_SECRET],
2168 secure: process.env.NODE_ENV === "production",
2169 },
2170});
2171
2172
2173export async function createUserSession({
2174 request,
2175 user,
2176 redirectTo,
2177}: {
2178 request: Request;
2179 user: User;
2180 redirectTo: string;
2181}) {
2182 const cookie = request.headers.get("Cookie");
2183 const session = await sessionStorage.getSession(cookie);
2184
2185
2186 session.set("user", user);
2187
2188
2189 return redirect(redirectTo, {
2190 headers: {
2191 "Set-Cookie": await sessionStorage.commitSession(session, {
2192 maxAge: 60 * 60 * 24 * 7, // 7 days
2193 }),
2194 },
2195 });
2196}
2197
2198
2199export async function getUserFromSession(request: Request) {
2200 const cookie = request.headers.get("Cookie");
2201 const session = await sessionStorage.getSession(cookie);
2202
2203
2204 return session.get("user") ?? null;
2205}
2206
2207
2208export async function requireUser(request: Request) {
2209 const user = await getUserFromSession(request);
2210
2211
2212 if (user === null) {
2213 throw redirect("/login");
2214 }
2215
2216
2217 return user;
2218}
2219```
2220
2221And also export the `User` model from the `models.ts` file right next to it:
2222
2223shared/api/models.ts
2224
2225```tsx
2226import type { components } from "./v1";
2227
2228
2229export type Article = components["schemas"]["Article"];
2230export type User = components["schemas"]["User"];
2231```
2232
2233Before 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:
2234
2235.env
2236
2237```bash
2238SESSION_SECRET=dontyoudarecopypastethis
2239```
2240
2241Finally, add some exports to the public API to make use of this code:
2242
2243shared/api/index.ts
2244
2245```tsx
2246export { GET, POST, PUT, DELETE } from "./client";
2247
2248
2249export type { Article } from "./models";
2250
2251
2252export { createUserSession, getUserFromSession, requireUser } from "./auth.server";
2253```
2254
2255Now 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:
2256
2257pages/sign-in/api/register.ts
2258
2259```tsx
2260import { json, type ActionFunctionArgs } from "@remix-run/node";
2261
2262
2263import { POST, createUserSession } from "shared/api";
2264
2265
2266export const register = async ({ request }: ActionFunctionArgs) => {
2267 const formData = await request.formData();
2268 const username = formData.get("username")?.toString() ?? "";
2269 const email = formData.get("email")?.toString() ?? "";
2270 const password = formData.get("password")?.toString() ?? "";
2271
2272
2273 const { data, error } = await POST("/users", {
2274 body: { user: { email, password, username } },
2275 });
2276
2277
2278 if (error) {
2279 return json({ error }, { status: 400 });
2280 } else {
2281 return createUserSession({
2282 request: request,
2283 user: data.user,
2284 redirectTo: "/",
2285 });
2286 }
2287};
2288```
2289
2290pages/sign-in/index.ts
2291
2292```tsx
2293export { RegisterPage } from './ui/RegisterPage';
2294export { register } from './api/register';
2295```
2296
2297Almost done! Just need to connect the page and action to the `/register` route. Create `register.tsx` in `app/routes`:
2298
2299app/routes/register.tsx
2300
2301```tsx
2302import { RegisterPage, register } from "pages/sign-in";
2303
2304
2305export { register as action };
2306
2307
2308export default RegisterPage;
2309```
2310
2311Now if you go to `http://localhost:3000/register`, you should be able to create a user! The rest of the application won’t react to this yet, we’ll address that momentarily.
2312
2313In a very similar way, we can implement the login page. Give it a try or just grab the code and move on:
2314
2315pages/sign-in/api/sign-in.ts
2316
2317```tsx
2318import { json, type ActionFunctionArgs } from "@remix-run/node";
2319
2320
2321import { POST, createUserSession } from "shared/api";
2322
2323
2324export const signIn = async ({ request }: ActionFunctionArgs) => {
2325 const formData = await request.formData();
2326 const email = formData.get("email")?.toString() ?? "";
2327 const password = formData.get("password")?.toString() ?? "";
2328
2329
2330 const { data, error } = await POST("/users/login", {
2331 body: { user: { email, password } },
2332 });
2333
2334
2335 if (error) {
2336 return json({ error }, { status: 400 });
2337 } else {
2338 return createUserSession({
2339 request: request,
2340 user: data.user,
2341 redirectTo: "/",
2342 });
2343 }
2344};
2345```
2346
2347pages/sign-in/ui/SignInPage.tsx
2348
2349```tsx
2350import { Form, Link, useActionData } from "@remix-run/react";
2351
2352
2353import type { signIn } from "../api/sign-in";
2354
2355
2356export function SignInPage() {
2357 const signInData = useActionData<typeof signIn>();
2358
2359
2360 return (
2361 <div className="auth-page">
2362 <div className="container page">
2363 <div className="row">
2364 <div className="col-md-6 offset-md-3 col-xs-12">
2365 <h1 className="text-xs-center">Sign in</h1>
2366 <p className="text-xs-center">
2367 <Link to="/register">Need an account?</Link>
2368 </p>
2369
2370
2371 {signInData?.error && (
2372 <ul className="error-messages">
2373 {signInData.error.errors.body.map((error) => (
2374 <li key={error}>{error}</li>
2375 ))}
2376 </ul>
2377 )}
2378
2379
2380 <Form method="post">
2381 <fieldset className="form-group">
2382 <input
2383 className="form-control form-control-lg"
2384 name="email"
2385 type="text"
2386 placeholder="Email"
2387 />
2388 </fieldset>
2389 <fieldset className="form-group">
2390 <input
2391 className="form-control form-control-lg"
2392 name="password"
2393 type="password"
2394 placeholder="Password"
2395 />
2396 </fieldset>
2397 <button className="btn btn-lg btn-primary pull-xs-right">
2398 Sign in
2399 </button>
2400 </Form>
2401 </div>
2402 </div>
2403 </div>
2404 </div>
2405 );
2406}
2407```
2408
2409pages/sign-in/index.ts
2410
2411```tsx
2412export { RegisterPage } from './ui/RegisterPage';
2413export { register } from './api/register';
2414export { SignInPage } from './ui/SignInPage';
2415export { signIn } from './api/sign-in';
2416```
2417
2418app/routes/login.tsx
2419
2420```tsx
2421import { SignInPage, signIn } from "pages/sign-in";
2422
2423
2424export { signIn as action };
2425
2426
2427export default SignInPage;
2428```
2429
2430Now let’s give the users a way to actually get to these pages.
2431
2432### Header
2433
2434[Section titled “Header”](#header)
2435
2436As 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:
2437
2438```bash
2439npx fsd shared ui
2440```
2441
2442Now create `shared/ui/Header.tsx` with the following contents:
2443
2444shared/ui/Header.tsx
2445
2446```tsx
2447import { useContext } from "react";
2448import { Link, useLocation } from "@remix-run/react";
2449
2450
2451import { CurrentUser } from "../api/currentUser";
2452
2453
2454export function Header() {
2455 const currentUser = useContext(CurrentUser);
2456 const { pathname } = useLocation();
2457
2458
2459 return (
2460 <nav className="navbar navbar-light">
2461 <div className="container">
2462 <Link className="navbar-brand" to="/" prefetch="intent">
2463 conduit
2464 </Link>
2465 <ul className="nav navbar-nav pull-xs-right">
2466 <li className="nav-item">
2467 <Link
2468 prefetch="intent"
2469 className={`nav-link ${pathname == "/" ? "active" : ""}`}
2470 to="/"
2471 >
2472 Home
2473 </Link>
2474 </li>
2475 {currentUser == null ? (
2476 <>
2477 <li className="nav-item">
2478 <Link
2479 prefetch="intent"
2480 className={`nav-link ${pathname == "/login" ? "active" : ""}`}
2481 to="/login"
2482 >
2483 Sign in
2484 </Link>
2485 </li>
2486 <li className="nav-item">
2487 <Link
2488 prefetch="intent"
2489 className={`nav-link ${pathname == "/register" ? "active" : ""}`}
2490 to="/register"
2491 >
2492 Sign up
2493 </Link>
2494 </li>
2495 </>
2496 ) : (
2497 <>
2498 <li className="nav-item">
2499 <Link
2500 prefetch="intent"
2501 className={`nav-link ${pathname == "/editor" ? "active" : ""}`}
2502 to="/editor"
2503 >
2504 <i className="ion-compose"></i> New Article{" "}
2505 </Link>
2506 </li>
2507
2508
2509 <li className="nav-item">
2510 <Link
2511 prefetch="intent"
2512 className={`nav-link ${pathname == "/settings" ? "active" : ""}`}
2513 to="/settings"
2514 >
2515 {" "}
2516 <i className="ion-gear-a"></i> Settings{" "}
2517 </Link>
2518 </li>
2519 <li className="nav-item">
2520 <Link
2521 prefetch="intent"
2522 className={`nav-link ${pathname.includes("/profile") ? "active" : ""}`}
2523 to={`/profile/${currentUser.username}`}
2524 >
2525 {currentUser.image && (
2526 <img
2527 width={25}
2528 height={25}
2529 src={currentUser.image}
2530 className="user-pic"
2531 alt=""
2532 />
2533 )}
2534 {currentUser.username}
2535 </Link>
2536 </li>
2537 </>
2538 )}
2539 </ul>
2540 </div>
2541 </nav>
2542 );
2543}
2544```
2545
2546Export this component from `shared/ui`:
2547
2548shared/ui/index.ts
2549
2550```tsx
2551export { Header } from "./Header";
2552```
2553
2554In the header, we rely on the context that’s kept in `shared/api`. Create that as well:
2555
2556shared/api/currentUser.ts
2557
2558```tsx
2559import { createContext } from "react";
2560
2561
2562import type { User } from "./models";
2563
2564
2565export const CurrentUser = createContext<User | null>(null);
2566```
2567
2568shared/api/index.ts
2569
2570```tsx
2571export { GET, POST, PUT, DELETE } from "./client";
2572
2573
2574export type { Article } from "./models";
2575
2576
2577export { createUserSession, getUserFromSession, requireUser } from "./auth.server";
2578export { CurrentUser } from "./currentUser";
2579```
2580
2581Now 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`:
2582
2583app/root.tsx
2584
2585```tsx
2586import { cssBundleHref } from "@remix-run/css-bundle";
2587import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
2588import {
2589 Links,
2590 LiveReload,
2591 Meta,
2592 Outlet,
2593 Scripts,
2594 ScrollRestoration,
2595 useLoaderData,
2596} from "@remix-run/react";
2597
2598
2599import { Header } from "shared/ui";
2600import { getUserFromSession, CurrentUser } from "shared/api";
2601
2602
2603export const links: LinksFunction = () => [
2604 ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
2605];
2606
2607
2608export const loader = ({ request }: LoaderFunctionArgs) =>
2609 getUserFromSession(request);
2610
2611
2612export default function App() {
2613 const user = useLoaderData<typeof loader>();
2614
2615
2616 return (
2617 <html lang="en">
2618 <head>
2619 <meta charSet="utf-8" />
2620 <meta name="viewport" content="width=device-width, initial-scale=1" />
2621 <Meta />
2622 <Links />
2623 <link
2624 href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
2625 rel="stylesheet"
2626 type="text/css"
2627 />
2628 <link
2629 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"
2630 rel="stylesheet"
2631 type="text/css"
2632 />
2633 <link rel="stylesheet" href="//demo.productionready.io/main.css" />
2634 <style>{`
2635 button {
2636 border: 0;
2637 }
2638 `}</style>
2639 </head>
2640 <body>
2641 <CurrentUser.Provider value={user}>
2642 <Header />
2643 <Outlet />
2644 </CurrentUser.Provider>
2645 <ScrollRestoration />
2646 <Scripts />
2647 <LiveReload />
2648 </body>
2649 </html>
2650 );
2651}
2652```
2653
2654At this point, you should end up with the following on the home page:
2655
2656
2657
2658The feed page of Conduit, including the header, the feed, and the tags. The tabs are still missing.
2659
2660### Tabs
2661
2662[Section titled “Tabs”](#tabs)
2663
2664Now 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:
2665
2666pages/feed/ui/Tabs.tsx
2667
2668```tsx
2669import { useContext } from "react";
2670import { Form, useSearchParams } from "@remix-run/react";
2671
2672
2673import { CurrentUser } from "shared/api";
2674
2675
2676export function Tabs() {
2677 const [searchParams] = useSearchParams();
2678 const currentUser = useContext(CurrentUser);
2679
2680
2681 return (
2682 <Form>
2683 <div className="feed-toggle">
2684 <ul className="nav nav-pills outline-active">
2685 {currentUser !== null && (
2686 <li className="nav-item">
2687 <button
2688 name="source"
2689 value="my-feed"
2690 className={`nav-link ${searchParams.get("source") === "my-feed" ? "active" : ""}`}
2691 >
2692 Your Feed
2693 </button>
2694 </li>
2695 )}
2696 <li className="nav-item">
2697 <button
2698 className={`nav-link ${searchParams.has("tag") || searchParams.has("source") ? "" : "active"}`}
2699 >
2700 Global Feed
2701 </button>
2702 </li>
2703 {searchParams.has("tag") && (
2704 <li className="nav-item">
2705 <span className="nav-link active">
2706 <i className="ion-pound"></i> {searchParams.get("tag")}
2707 </span>
2708 </li>
2709 )}
2710 </ul>
2711 </div>
2712 </Form>
2713 );
2714}
2715```
2716
2717pages/feed/ui/PopularTags.tsx
2718
2719```tsx
2720import { Form, useLoaderData } from "@remix-run/react";
2721import { ExistingSearchParams } from "remix-utils/existing-search-params";
2722
2723
2724import type { loader } from "../api/loader";
2725
2726
2727export function PopularTags() {
2728 const { tags } = useLoaderData<typeof loader>();
2729
2730
2731 return (
2732 <div className="sidebar">
2733 <p>Popular Tags</p>
2734
2735
2736 <Form>
2737 <ExistingSearchParams exclude={["tag", "page", "source"]} />
2738 <div className="tag-list">
2739 {tags.tags.map((tag) => (
2740 <button
2741 key={tag}
2742 name="tag"
2743 value={tag}
2744 className="tag-pill tag-default"
2745 >
2746 {tag}
2747 </button>
2748 ))}
2749 </div>
2750 </Form>
2751 </div>
2752 );
2753}
2754```
2755
2756pages/feed/ui/Pagination.tsx
2757
2758```tsx
2759import { Form, useLoaderData, useSearchParams } from "@remix-run/react";
2760import { ExistingSearchParams } from "remix-utils/existing-search-params";
2761
2762
2763import { LIMIT, type loader } from "../api/loader";
2764
2765
2766export function Pagination() {
2767 const [searchParams] = useSearchParams();
2768 const { articles } = useLoaderData<typeof loader>();
2769 const pageAmount = Math.ceil(articles.articlesCount / LIMIT);
2770 const currentPage = parseInt(searchParams.get("page") ?? "1", 10);
2771
2772
2773 return (
2774 <Form>
2775 <ExistingSearchParams exclude={["page"]} />
2776 <ul className="pagination">
2777 {Array(pageAmount)
2778 .fill(null)
2779 .map((_, index) =>
2780 index + 1 === currentPage ? (
2781 <li key={index} className="page-item active">
2782 <span className="page-link">{index + 1}</span>
2783 </li>
2784 ) : (
2785 <li key={index} className="page-item">
2786 <button className="page-link" name="page" value={index + 1}>
2787 {index + 1}
2788 </button>
2789 </li>
2790 ),
2791 )}
2792 </ul>
2793 </Form>
2794 );
2795}
2796```
2797
2798And now we can significantly simplify the feed page itself:
2799
2800pages/feed/ui/FeedPage.tsx
2801
2802```tsx
2803import { useLoaderData } from "@remix-run/react";
2804
2805
2806import type { loader } from "../api/loader";
2807import { ArticlePreview } from "./ArticlePreview";
2808import { Tabs } from "./Tabs";
2809import { PopularTags } from "./PopularTags";
2810import { Pagination } from "./Pagination";
2811
2812
2813export function FeedPage() {
2814 const { articles } = useLoaderData<typeof loader>();
2815
2816
2817 return (
2818 <div className="home-page">
2819 <div className="banner">
2820 <div className="container">
2821 <h1 className="logo-font">conduit</h1>
2822 <p>A place to share your knowledge.</p>
2823 </div>
2824 </div>
2825
2826
2827 <div className="container page">
2828 <div className="row">
2829 <div className="col-md-9">
2830 <Tabs />
2831
2832
2833 {articles.articles.map((article) => (
2834 <ArticlePreview key={article.slug} article={article} />
2835 ))}
2836
2837
2838 <Pagination />
2839 </div>
2840
2841
2842 <div className="col-md-3">
2843 <PopularTags />
2844 </div>
2845 </div>
2846 </div>
2847 </div>
2848 );
2849}
2850```
2851
2852We also need to account for the new tab in the loader function:
2853
2854pages/feed/api/loader.ts
2855
2856```tsx
2857import { json, type LoaderFunctionArgs } from "@remix-run/node";
2858import type { FetchResponse } from "openapi-fetch";
2859import { promiseHash } from "remix-utils/promise";
2860
2861
2862import { GET, requireUser } from "shared/api";
2863
2864
2865async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
2866 responsePromise: Promise<FetchResponse<T, O, Media>>,
2867) {
2868 /* unchanged */
2869}
2870
2871
2872/** Amount of articles on one page. */
2873export const LIMIT = 20;
2874
2875
2876export const loader = async ({ request }: LoaderFunctionArgs) => {
2877 const url = new URL(request.url);
2878 const selectedTag = url.searchParams.get("tag") ?? undefined;
2879 const page = parseInt(url.searchParams.get("page") ?? "", 10);
2880
2881
2882 if (url.searchParams.get("source") === "my-feed") {
2883 const userSession = await requireUser(request);
2884
2885
2886 return json(
2887 await promiseHash({
2888 articles: throwAnyErrors(
2889 GET("/articles/feed", {
2890 params: {
2891 query: {
2892 limit: LIMIT,
2893 offset: !Number.isNaN(page) ? page * LIMIT : undefined,
2894 },
2895 },
2896 headers: { Authorization: `Token ${userSession.token}` },
2897 }),
2898 ),
2899 tags: throwAnyErrors(GET("/tags")),
2900 }),
2901 );
2902 }
2903
2904
2905 return json(
2906 await promiseHash({
2907 articles: throwAnyErrors(
2908 GET("/articles", {
2909 params: {
2910 query: {
2911 tag: selectedTag,
2912 limit: LIMIT,
2913 offset: !Number.isNaN(page) ? page * LIMIT : undefined,
2914 },
2915 },
2916 }),
2917 ),
2918 tags: throwAnyErrors(GET("/tags")),
2919 }),
2920 );
2921};
2922```
2923
2924Before we leave the feed page, let’s add some code that handles likes to posts. Change your `ArticlePreview.tsx` to the following:
2925
2926pages/feed/ui/ArticlePreview\.tsx
2927
2928```tsx
2929import { Form, Link } from "@remix-run/react";
2930import type { Article } from "shared/api";
2931
2932
2933interface ArticlePreviewProps {
2934 article: Article;
2935}
2936
2937
2938export function ArticlePreview({ article }: ArticlePreviewProps) {
2939 return (
2940 <div className="article-preview">
2941 <div className="article-meta">
2942 <Link to={`/profile/${article.author.username}`} prefetch="intent">
2943 <img src={article.author.image} alt="" />
2944 </Link>
2945 <div className="info">
2946 <Link
2947 to={`/profile/${article.author.username}`}
2948 className="author"
2949 prefetch="intent"
2950 >
2951 {article.author.username}
2952 </Link>
2953 <span className="date" suppressHydrationWarning>
2954 {new Date(article.createdAt).toLocaleDateString(undefined, {
2955 dateStyle: "long",
2956 })}
2957 </span>
2958 </div>
2959 <Form
2960 method="post"
2961 action={`/article/${article.slug}`}
2962 preventScrollReset
2963 >
2964 <button
2965 name="_action"
2966 value={article.favorited ? "unfavorite" : "favorite"}
2967 className={`btn ${article.favorited ? "btn-primary" : "btn-outline-primary"} btn-sm pull-xs-right`}
2968 >
2969 <i className="ion-heart"></i> {article.favoritesCount}
2970 </button>
2971 </Form>
2972 </div>
2973 <Link
2974 to={`/article/${article.slug}`}
2975 className="preview-link"
2976 prefetch="intent"
2977 >
2978 <h1>{article.title}</h1>
2979 <p>{article.description}</p>
2980 <span>Read more...</span>
2981 <ul className="tag-list">
2982 {article.tagList.map((tag) => (
2983 <li key={tag} className="tag-default tag-pill tag-outline">
2984 {tag}
2985 </li>
2986 ))}
2987 </ul>
2988 </Link>
2989 </div>
2990 );
2991}
2992```
2993
2994This 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.
2995
2996And with that we are officially done with the feed! Yay!
2997
2998### Article reader
2999
3000[Section titled “Article reader”](#article-reader)
3001
3002First, we need data. Let’s create a loader:
3003
3004```bash
3005npx fsd pages article-read -s api
3006```
3007
3008pages/article-read/api/loader.ts
3009
3010```tsx
3011import { json, type LoaderFunctionArgs } from "@remix-run/node";
3012import invariant from "tiny-invariant";
3013import type { FetchResponse } from "openapi-fetch";
3014import { promiseHash } from "remix-utils/promise";
3015
3016
3017import { GET, getUserFromSession } from "shared/api";
3018
3019
3020async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
3021 responsePromise: Promise<FetchResponse<T, O, Media>>,
3022) {
3023 const { data, error, response } = await responsePromise;
3024
3025
3026 if (error !== undefined) {
3027 throw json(error, { status: response.status });
3028 }
3029
3030
3031 return data as NonNullable<typeof data>;
3032}
3033
3034
3035export const loader = async ({ request, params }: LoaderFunctionArgs) => {
3036 invariant(params.slug, "Expected a slug parameter");
3037 const currentUser = await getUserFromSession(request);
3038 const authorization = currentUser
3039 ? { Authorization: `Token ${currentUser.token}` }
3040 : undefined;
3041
3042
3043 return json(
3044 await promiseHash({
3045 article: throwAnyErrors(
3046 GET("/articles/{slug}", {
3047 params: {
3048 path: { slug: params.slug },
3049 },
3050 headers: authorization,
3051 }),
3052 ),
3053 comments: throwAnyErrors(
3054 GET("/articles/{slug}/comments", {
3055 params: {
3056 path: { slug: params.slug },
3057 },
3058 headers: authorization,
3059 }),
3060 ),
3061 }),
3062 );
3063};
3064```
3065
3066pages/article-read/index.ts
3067
3068```tsx
3069export { loader } from "./api/loader";
3070```
3071
3072Now we can connect it to the route `/article/:slug` by creating the a route file called `article.$slug.tsx`:
3073
3074app/routes/article.$slug.tsx
3075
3076```tsx
3077export { loader } from "pages/article-read";
3078```
3079
3080The 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:
3081
3082pages/article-read/ui/ArticleReadPage.tsx
3083
3084```tsx
3085import { useLoaderData } from "@remix-run/react";
3086
3087
3088import type { loader } from "../api/loader";
3089import { ArticleMeta } from "./ArticleMeta";
3090import { Comments } from "./Comments";
3091
3092
3093export function ArticleReadPage() {
3094 const { article } = useLoaderData<typeof loader>();
3095
3096
3097 return (
3098 <div className="article-page">
3099 <div className="banner">
3100 <div className="container">
3101 <h1>{article.article.title}</h1>
3102
3103
3104 <ArticleMeta />
3105 </div>
3106 </div>
3107
3108
3109 <div className="container page">
3110 <div className="row article-content">
3111 <div className="col-md-12">
3112 <p>{article.article.body}</p>
3113 <ul className="tag-list">
3114 {article.article.tagList.map((tag) => (
3115 <li className="tag-default tag-pill tag-outline" key={tag}>
3116 {tag}
3117 </li>
3118 ))}
3119 </ul>
3120 </div>
3121 </div>
3122
3123
3124 <hr />
3125
3126
3127 <div className="article-actions">
3128 <ArticleMeta />
3129 </div>
3130
3131
3132 <div className="row">
3133 <Comments />
3134 </div>
3135 </div>
3136 </div>
3137 );
3138}
3139```
3140
3141What’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:
3142
3143pages/article-read/api/action.ts
3144
3145```tsx
3146import { redirect, type ActionFunctionArgs } from "@remix-run/node";
3147import { namedAction } from "remix-utils/named-action";
3148import { redirectBack } from "remix-utils/redirect-back";
3149import invariant from "tiny-invariant";
3150
3151
3152import { DELETE, POST, requireUser } from "shared/api";
3153
3154
3155export const action = async ({ request, params }: ActionFunctionArgs) => {
3156 const currentUser = await requireUser(request);
3157
3158
3159 const authorization = { Authorization: `Token ${currentUser.token}` };
3160
3161
3162 const formData = await request.formData();
3163
3164
3165 return namedAction(formData, {
3166 async delete() {
3167 invariant(params.slug, "Expected a slug parameter");
3168 await DELETE("/articles/{slug}", {
3169 params: { path: { slug: params.slug } },
3170 headers: authorization,
3171 });
3172 return redirect("/");
3173 },
3174 async favorite() {
3175 invariant(params.slug, "Expected a slug parameter");
3176 await POST("/articles/{slug}/favorite", {
3177 params: { path: { slug: params.slug } },
3178 headers: authorization,
3179 });
3180 return redirectBack(request, { fallback: "/" });
3181 },
3182 async unfavorite() {
3183 invariant(params.slug, "Expected a slug parameter");
3184 await DELETE("/articles/{slug}/favorite", {
3185 params: { path: { slug: params.slug } },
3186 headers: authorization,
3187 });
3188 return redirectBack(request, { fallback: "/" });
3189 },
3190 async createComment() {
3191 invariant(params.slug, "Expected a slug parameter");
3192 const comment = formData.get("comment");
3193 invariant(typeof comment === "string", "Expected a comment parameter");
3194 await POST("/articles/{slug}/comments", {
3195 params: { path: { slug: params.slug } },
3196 headers: { ...authorization, "Content-Type": "application/json" },
3197 body: { comment: { body: comment } },
3198 });
3199 return redirectBack(request, { fallback: "/" });
3200 },
3201 async deleteComment() {
3202 invariant(params.slug, "Expected a slug parameter");
3203 const commentId = formData.get("id");
3204 invariant(typeof commentId === "string", "Expected an id parameter");
3205 const commentIdNumeric = parseInt(commentId, 10);
3206 invariant(
3207 !Number.isNaN(commentIdNumeric),
3208 "Expected a numeric id parameter",
3209 );
3210 await DELETE("/articles/{slug}/comments/{id}", {
3211 params: { path: { slug: params.slug, id: commentIdNumeric } },
3212 headers: authorization,
3213 });
3214 return redirectBack(request, { fallback: "/" });
3215 },
3216 async followAuthor() {
3217 const authorUsername = formData.get("username");
3218 invariant(
3219 typeof authorUsername === "string",
3220 "Expected a username parameter",
3221 );
3222 await POST("/profiles/{username}/follow", {
3223 params: { path: { username: authorUsername } },
3224 headers: authorization,
3225 });
3226 return redirectBack(request, { fallback: "/" });
3227 },
3228 async unfollowAuthor() {
3229 const authorUsername = formData.get("username");
3230 invariant(
3231 typeof authorUsername === "string",
3232 "Expected a username parameter",
3233 );
3234 await DELETE("/profiles/{username}/follow", {
3235 params: { path: { username: authorUsername } },
3236 headers: authorization,
3237 });
3238 return redirectBack(request, { fallback: "/" });
3239 },
3240 });
3241};
3242```
3243
3244Export that from the slice and then from the route. While we’re at it, let’s also connect the page itself:
3245
3246pages/article-read/index.ts
3247
3248```tsx
3249export { ArticleReadPage } from "./ui/ArticleReadPage";
3250export { loader } from "./api/loader";
3251export { action } from "./api/action";
3252```
3253
3254app/routes/article.$slug.tsx
3255
3256```tsx
3257import { ArticleReadPage } from "pages/article-read";
3258
3259
3260export { loader, action } from "pages/article-read";
3261
3262
3263export default ArticleReadPage;
3264```
3265
3266Now, 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.
3267
3268`ArticleMeta` and `Comments` are, again, a bunch of forms. We’ve done this before, let’s grab their code and move on:
3269
3270pages/article-read/ui/ArticleMeta.tsx
3271
3272```tsx
3273import { Form, Link, useLoaderData } from "@remix-run/react";
3274import { useContext } from "react";
3275
3276
3277import { CurrentUser } from "shared/api";
3278import type { loader } from "../api/loader";
3279
3280
3281export function ArticleMeta() {
3282 const currentUser = useContext(CurrentUser);
3283 const { article } = useLoaderData<typeof loader>();
3284
3285
3286 return (
3287 <Form method="post">
3288 <div className="article-meta">
3289 <Link
3290 prefetch="intent"
3291 to={`/profile/${article.article.author.username}`}
3292 >
3293 <img src={article.article.author.image} alt="" />
3294 </Link>
3295
3296
3297 <div className="info">
3298 <Link
3299 prefetch="intent"
3300 to={`/profile/${article.article.author.username}`}
3301 className="author"
3302 >
3303 {article.article.author.username}
3304 </Link>
3305 <span className="date">{article.article.createdAt}</span>
3306 </div>
3307
3308
3309 {article.article.author.username == currentUser?.username ? (
3310 <>
3311 <Link
3312 prefetch="intent"
3313 to={`/editor/${article.article.slug}`}
3314 className="btn btn-sm btn-outline-secondary"
3315 >
3316 <i className="ion-edit"></i> Edit Article
3317 </Link>
3318
3319 <button
3320 name="_action"
3321 value="delete"
3322 className="btn btn-sm btn-outline-danger"
3323 >
3324 <i className="ion-trash-a"></i> Delete Article
3325 </button>
3326 </>
3327 ) : (
3328 <>
3329 <input
3330 name="username"
3331 value={article.article.author.username}
3332 type="hidden"
3333 />
3334 <button
3335 name="_action"
3336 value={
3337 article.article.author.following
3338 ? "unfollowAuthor"
3339 : "followAuthor"
3340 }
3341 className={`btn btn-sm ${article.article.author.following ? "btn-secondary" : "btn-outline-secondary"}`}
3342 >
3343 <i className="ion-plus-round"></i>
3344 {" "}
3345 {article.article.author.following
3346 ? "Unfollow"
3347 : "Follow"}{" "}
3348 {article.article.author.username}
3349 </button>
3350
3351 <button
3352 name="_action"
3353 value={article.article.favorited ? "unfavorite" : "favorite"}
3354 className={`btn btn-sm ${article.article.favorited ? "btn-primary" : "btn-outline-primary"}`}
3355 >
3356 <i className="ion-heart"></i>
3357 {article.article.favorited
3358 ? "Unfavorite"
3359 : "Favorite"}{" "}
3360 Post{" "}
3361 <span className="counter">
3362 ({article.article.favoritesCount})
3363 </span>
3364 </button>
3365 </>
3366 )}
3367 </div>
3368 </Form>
3369 );
3370}
3371```
3372
3373pages/article-read/ui/Comments.tsx
3374
3375```tsx
3376import { useContext } from "react";
3377import { Form, Link, useLoaderData } from "@remix-run/react";
3378
3379
3380import { CurrentUser } from "shared/api";
3381import type { loader } from "../api/loader";
3382
3383
3384export function Comments() {
3385 const { comments } = useLoaderData<typeof loader>();
3386 const currentUser = useContext(CurrentUser);
3387
3388
3389 return (
3390 <div className="col-xs-12 col-md-8 offset-md-2">
3391 {currentUser !== null ? (
3392 <Form
3393 preventScrollReset={true}
3394 method="post"
3395 className="card comment-form"
3396 >
3397 <div className="card-block">
3398 <textarea
3399 required
3400 className="form-control"
3401 name="comment"
3402 placeholder="Write a comment..."
3403 rows={3}
3404 ></textarea>
3405 </div>
3406 <div className="card-footer">
3407 <img
3408 src={currentUser.image}
3409 className="comment-author-img"
3410 alt=""
3411 />
3412 <button
3413 className="btn btn-sm btn-primary"
3414 name="_action"
3415 value="createComment"
3416 >
3417 Post Comment
3418 </button>
3419 </div>
3420 </Form>
3421 ) : (
3422 <div className="row">
3423 <div className="col-xs-12 col-md-8 offset-md-2">
3424 <p>
3425 <Link to="/login">Sign in</Link>
3426 or
3427 <Link to="/register">Sign up</Link>
3428 to add comments on this article.
3429 </p>
3430 </div>
3431 </div>
3432 )}
3433
3434
3435 {comments.comments.map((comment) => (
3436 <div className="card" key={comment.id}>
3437 <div className="card-block">
3438 <p className="card-text">{comment.body}</p>
3439 </div>
3440
3441
3442 <div className="card-footer">
3443 <Link
3444 to={`/profile/${comment.author.username}`}
3445 className="comment-author"
3446 >
3447 <img
3448 src={comment.author.image}
3449 className="comment-author-img"
3450 alt=""
3451 />
3452 </Link>
3453
3454 <Link
3455 to={`/profile/${comment.author.username}`}
3456 className="comment-author"
3457 >
3458 {comment.author.username}
3459 </Link>
3460 <span className="date-posted">{comment.createdAt}</span>
3461 {comment.author.username === currentUser?.username && (
3462 <span className="mod-options">
3463 <Form method="post" preventScrollReset={true}>
3464 <input type="hidden" name="id" value={comment.id} />
3465 <button
3466 name="_action"
3467 value="deleteComment"
3468 style={{
3469 border: "none",
3470 outline: "none",
3471 backgroundColor: "transparent",
3472 }}
3473 >
3474 <i className="ion-trash-a"></i>
3475 </button>
3476 </Form>
3477 </span>
3478 )}
3479 </div>
3480 </div>
3481 ))}
3482 </div>
3483 );
3484}
3485```
3486
3487And 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.
3488
3489
3490
3491Article reader with functioning buttons to like and follow
3492
3493### Article editor
3494
3495[Section titled “Article editor”](#article-editor)
3496
3497This 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.
3498
3499The page itself, `article-edit/ui/ArticleEditPage.tsx`, will be quite simple, extra complexity stowed away into two other components:
3500
3501pages/article-edit/ui/ArticleEditPage.tsx
3502
3503```tsx
3504import { Form, useLoaderData } from "@remix-run/react";
3505
3506
3507import type { loader } from "../api/loader";
3508import { TagsInput } from "./TagsInput";
3509import { FormErrors } from "./FormErrors";
3510
3511
3512export function ArticleEditPage() {
3513 const article = useLoaderData<typeof loader>();
3514
3515
3516 return (
3517 <div className="editor-page">
3518 <div className="container page">
3519 <div className="row">
3520 <div className="col-md-10 offset-md-1 col-xs-12">
3521 <FormErrors />
3522
3523
3524 <Form method="post">
3525 <fieldset>
3526 <fieldset className="form-group">
3527 <input
3528 type="text"
3529 className="form-control form-control-lg"
3530 name="title"
3531 placeholder="Article Title"
3532 defaultValue={article.article?.title}
3533 />
3534 </fieldset>
3535 <fieldset className="form-group">
3536 <input
3537 type="text"
3538 className="form-control"
3539 name="description"
3540 placeholder="What's this article about?"
3541 defaultValue={article.article?.description}
3542 />
3543 </fieldset>
3544 <fieldset className="form-group">
3545 <textarea
3546 className="form-control"
3547 name="body"
3548 rows={8}
3549 placeholder="Write your article (in markdown)"
3550 defaultValue={article.article?.body}
3551 ></textarea>
3552 </fieldset>
3553 <fieldset className="form-group">
3554 <TagsInput
3555 name="tags"
3556 defaultValue={article.article?.tagList ?? []}
3557 />
3558 </fieldset>
3559
3560
3561 <button className="btn btn-lg pull-xs-right btn-primary">
3562 Publish Article
3563 </button>
3564 </fieldset>
3565 </Form>
3566 </div>
3567 </div>
3568 </div>
3569 </div>
3570 );
3571}
3572```
3573
3574This 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:
3575
3576pages/article-edit/ui/FormErrors.tsx
3577
3578```tsx
3579import { useActionData } from "@remix-run/react";
3580import type { action } from "../api/action";
3581
3582
3583export function FormErrors() {
3584 const actionData = useActionData<typeof action>();
3585
3586
3587 return actionData?.errors != null ? (
3588 <ul className="error-messages">
3589 {actionData.errors.map((error) => (
3590 <li key={error}>{error}</li>
3591 ))}
3592 </ul>
3593 ) : null;
3594}
3595```
3596
3597Here 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.
3598
3599Another component is the tags input. It’s just a plain input field with an additional preview of chosen tags. Not much to see here:
3600
3601pages/article-edit/ui/TagsInput.tsx
3602
3603```tsx
3604import { useEffect, useRef, useState } from "react";
3605
3606
3607export function TagsInput({
3608 name,
3609 defaultValue,
3610}: {
3611 name: string;
3612 defaultValue?: Array<string>;
3613}) {
3614 const [tagListState, setTagListState] = useState(defaultValue ?? []);
3615
3616
3617 function removeTag(tag: string): void {
3618 const newTagList = tagListState.filter((t) => t !== tag);
3619 setTagListState(newTagList);
3620 }
3621
3622
3623 const tagsInput = useRef<HTMLInputElement>(null);
3624 useEffect(() => {
3625 tagsInput.current && (tagsInput.current.value = tagListState.join(","));
3626 }, [tagListState]);
3627
3628
3629 return (
3630 <>
3631 <input
3632 type="text"
3633 className="form-control"
3634 id="tags"
3635 name={name}
3636 placeholder="Enter tags"
3637 defaultValue={tagListState.join(",")}
3638 onChange={(e) =>
3639 setTagListState(e.target.value.split(",").filter(Boolean))
3640 }
3641 />
3642 <div className="tag-list">
3643 {tagListState.map((tag) => (
3644 <span className="tag-default tag-pill" key={tag}>
3645 <i
3646 className="ion-close-round"
3647 role="button"
3648 tabIndex={0}
3649 onKeyDown={(e) =>
3650 [" ", "Enter"].includes(e.key) && removeTag(tag)
3651 }
3652 onClick={() => removeTag(tag)}
3653 ></i>{" "}
3654 {tag}
3655 </span>
3656 ))}
3657 </div>
3658 </>
3659 );
3660}
3661```
3662
3663Now, 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:
3664
3665pages/article-edit/api/loader.ts
3666
3667```ts
3668import { json, type LoaderFunctionArgs } from "@remix-run/node";
3669import type { FetchResponse } from "openapi-fetch";
3670
3671
3672import { GET, requireUser } from "shared/api";
3673
3674
3675async function throwAnyErrors<T, O, Media extends `${string}/${string}`>(
3676 responsePromise: Promise<FetchResponse<T, O, Media>>,
3677) {
3678 const { data, error, response } = await responsePromise;
3679
3680
3681 if (error !== undefined) {
3682 throw json(error, { status: response.status });
3683 }
3684
3685
3686 return data as NonNullable<typeof data>;
3687}
3688
3689
3690export const loader = async ({ params, request }: LoaderFunctionArgs) => {
3691 const currentUser = await requireUser(request);
3692
3693
3694 if (!params.slug) {
3695 return { article: null };
3696 }
3697
3698
3699 return throwAnyErrors(
3700 GET("/articles/{slug}", {
3701 params: { path: { slug: params.slug } },
3702 headers: { Authorization: `Token ${currentUser.token}` },
3703 }),
3704 );
3705};
3706```
3707
3708The 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:
3709
3710pages/article-edit/api/action.ts
3711
3712```tsx
3713import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
3714
3715
3716import { POST, PUT, requireUser } from "shared/api";
3717import { parseAsArticle } from "../model/parseAsArticle";
3718
3719
3720export const action = async ({ request, params }: ActionFunctionArgs) => {
3721 try {
3722 const { body, description, title, tags } = parseAsArticle(
3723 await request.formData(),
3724 );
3725 const tagList = tags?.split(",") ?? [];
3726
3727
3728 const currentUser = await requireUser(request);
3729 const payload = {
3730 body: {
3731 article: {
3732 title,
3733 description,
3734 body,
3735 tagList,
3736 },
3737 },
3738 headers: { Authorization: `Token ${currentUser.token}` },
3739 };
3740
3741
3742 const { data, error } = await (params.slug
3743 ? PUT("/articles/{slug}", {
3744 params: { path: { slug: params.slug } },
3745 ...payload,
3746 })
3747 : POST("/articles", payload));
3748
3749
3750 if (error) {
3751 return json({ errors: error }, { status: 422 });
3752 }
3753
3754
3755 return redirect(`/article/${data.article.slug ?? ""}`);
3756 } catch (errors) {
3757 return json({ errors }, { status: 400 });
3758 }
3759};
3760```
3761
3762The 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:
3763
3764pages/article-edit/model/parseAsArticle.ts
3765
3766```tsx
3767export function parseAsArticle(data: FormData) {
3768 const errors = [];
3769
3770
3771 const title = data.get("title");
3772 if (typeof title !== "string" || title === "") {
3773 errors.push("Give this article a title");
3774 }
3775
3776
3777 const description = data.get("description");
3778 if (typeof description !== "string" || description === "") {
3779 errors.push("Describe what this article is about");
3780 }
3781
3782
3783 const body = data.get("body");
3784 if (typeof body !== "string" || body === "") {
3785 errors.push("Write the article itself");
3786 }
3787
3788
3789 const tags = data.get("tags");
3790 if (typeof tags !== "string") {
3791 errors.push("The tags must be a string");
3792 }
3793
3794
3795 if (errors.length > 0) {
3796 throw errors;
3797 }
3798
3799
3800 return { title, description, body, tags: data.get("tags") ?? "" } as {
3801 title: string;
3802 description: string;
3803 body: string;
3804 tags: string;
3805 };
3806}
3807```
3808
3809Arguably, 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.
3810
3811One 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`:
3812
3813pages/article-edit/index.ts
3814
3815```tsx
3816export { ArticleEditPage } from "./ui/ArticleEditPage";
3817export { loader } from "./api/loader";
3818export { action } from "./api/action";
3819```
3820
3821app/routes/editor.\_index.tsx, app/routes/editor.$slug.tsx (same content)
3822
3823```tsx
3824import { ArticleEditPage } from "pages/article-edit";
3825
3826
3827export { loader, action } from "pages/article-edit";
3828
3829
3830export default ArticleEditPage;
3831```
3832
3833We’re done now! Log in and try creating a new article. Or “forget” to write the article and see the validation kick in.
3834
3835
3836
3837The 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”**.
3838
3839The 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 :)
3840
3841# Handling API Requests
3842
3843## Shared API Requests
3844
3845[Section titled “Shared API Requests”](#shared-api-requests)
3846
3847Start by placing common API request logic in the `shared/api` directory. This makes it easy to reuse requests across your application and helps with faster prototyping. For many projects, this is all you’ll need for API calls.
3848
3849A typical file structure would be:
3850
3851* shared/
3852
3853 * client.ts
3854
3855 * index.ts
3856
3857 * endpoints/
3858
3859 * login.ts
3860
3861The `client.ts` file centralizes your HTTP request setup. It wraps your chosen method (like `fetch()` or an `axios` instance) and handles common configurations, such as:
3862
3863* Backend base URL.
3864* Default headers (e.g., for authentication).
3865* Data serialization.
3866
3867Here are examples for `axios` and `fetch`:
3868
3869* Axios
3870
3871 shared/api/client.ts
3872
3873 ```ts
3874 // Example using axios
3875 import axios from 'axios';
3876
3877
3878 export const client = axios.create({
3879 baseURL: 'https://your-api-domain.com/api/',
3880 timeout: 5000,
3881 headers: { 'X-Custom-Header': 'my-custom-value' }
3882 });
3883 ```
3884
3885* Fetch
3886
3887 shared/api/client.ts
3888
3889 ```ts
3890 export const client = {
3891 async post(endpoint: string, body: any, options?: RequestInit) {
3892 const response = await fetch(`https://your-api-domain.com/api${endpoint}`, {
3893 method: 'POST',
3894 body: JSON.stringify(body),
3895 ...options,
3896 headers: {
3897 'Content-Type': 'application/json',
3898 'X-Custom-Header': 'my-custom-value',
3899 ...options?.headers,
3900 },
3901 });
3902 return response.json();
3903 }
3904 // ... other methods like put, delete, etc.
3905 };
3906 ```
3907
3908Organize your individual API request functions in `shared/api/endpoints`, grouping them by the API endpoint.
3909
3910Note
3911
3912To keep examples focused, we omit form interaction and validation. For details on libraries like Zod or Valibot, refer to the [Type Validation and Schemas](/docs/guides/examples/types#type-validation-schemas-and-zod) article.
3913
3914shared/api/endpoints/login.ts
3915
3916```ts
3917import { client } from '../client';
3918
3919
3920export interface LoginCredentials {
3921 email: string;
3922 password: string;
3923}
3924
3925
3926export function login(credentials: LoginCredentials) {
3927 return client.post('/login', credentials);
3928}
3929```
3930
3931Use an `index.ts` file in `shared/api` to export your request functions.
3932
3933shared/api/index.ts
3934
3935```ts
3936export { client } from './client'; // If you want to export the client itself
3937export { login } from './endpoints/login';
3938export type { LoginCredentials } from './endpoints/login';
3939```
3940
3941## Slice-Specific API Requests
3942
3943[Section titled “Slice-Specific API Requests”](#slice-specific-api-requests)
3944
3945If an API request is only used by a specific slice (like a single page or feature) and won’t be reused, place it in the api segment of that slice. This keeps slice-specific logic neatly contained.
3946
3947* pages/
3948
3949 * login/
3950
3951 * index.ts
3952
3953 * api/
3954
3955 * login.ts
3956
3957 * ui/
3958
3959 * LoginPage.tsx
3960
3961pages/login/api/login.ts
3962
3963```ts
3964import { client } from 'shared/api';
3965
3966
3967interface LoginCredentials {
3968 email: string;
3969 password: string;
3970}
3971
3972
3973export function login(credentials: LoginCredentials) {
3974 return client.post('/login', credentials);
3975}
3976```
3977
3978You don’t need to export `login()` function in the page’s public API, because it’s unlikely that any other place in the app will need this request.
3979
3980Note
3981
3982Avoid placing API calls and response types in the `entities` layer prematurely. Backend responses may differ from what your frontend entities need. API logic in `shared/api` or a slice’s `api` segment allows you to transform data appropriately, keeping entities focused on frontend concerns.
3983
3984## Using Client Generators
3985
3986[Section titled “Using Client Generators”](#client-generators)
3987
3988If your backend has an OpenAPI specification, tools like [orval](https://orval.dev/) or [openapi-typescript](https://openapi-ts.dev/) can generate API types and request functions for you. Place the generated code in, for example, `shared/api/openapi`. Make sure to include `README.md` to document what those types are, and how to generate them.
3989
3990## Integrating with Server State Libraries
3991
3992[Section titled “Integrating with Server State Libraries”](#server-state-libraries)
3993
3994When using server state libraries like [TanStack Query (React Query)](https://tanstack.com/query/latest) or [Pinia Colada](https://pinia-colada.esm.dev/) you might need to share types or cache keys between slices. Use the `shared` layer for things like:
3995
3996* API data types
3997* Cache keys
3998* Common query/mutation options
3999
4000For more details on how to work with server state libraries, refer to [React Query article](/docs/guides/tech/with-react-query)
4001
4002# Authentication
4003
4004Broadly, authentication consists of the following steps:
4005
40061. Get the credentials from the user
40072. Send them to the backend
40083. Store the token to make authenticated requests
4009
4010## How to get credentials from the user
4011
4012[Section titled “How to get credentials from the user”](#how-to-get-credentials-from-the-user)
4013
4014We 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 [step 3](#how-to-store-the-token-for-authenticated-requests).
4015
4016### Dedicated page for login
4017
4018[Section titled “Dedicated page for login”](#dedicated-page-for-login)
4019
4020Usually, 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:
4021
4022* pages/
4023
4024 * login/
4025
4026 * ui/
4027
4028 * LoginPage.tsx
4029 * RegisterPage.tsx
4030 * index.ts
4031 * …
4032
4033Here 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.
4034
4035### Dialog for login
4036
4037[Section titled “Dialog for login”](#dialog-for-login)
4038
4039If 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.
4040
4041* widgets/
4042
4043 * login-dialog/
4044
4045 * ui/
4046
4047 * LoginDialog.tsx
4048
4049 * index.ts
4050
4051 * …
4052
4053The rest of this guide is written for the dedicated page approach, but the same principles apply to the dialog widget.
4054
4055### Client-side validation
4056
4057[Section titled “Client-side validation”](#client-side-validation)
4058
4059Sometimes, 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, [Zod](https://zod.dev) for JS/TS, and expose that schema to the `ui` segment:
4060
4061pages/login/model/registration-schema.ts
4062
4063```ts
4064import { z } from "zod";
4065
4066
4067export const registrationData = z.object({
4068 email: z.string().email(),
4069 password: z.string().min(6),
4070 confirmPassword: z.string(),
4071}).refine((data) => data.password === data.confirmPassword, {
4072 message: "Passwords do not match",
4073 path: ["confirmPassword"],
4074});
4075```
4076
4077Then, in the `ui` segment, you can use this schema to validate the user input:
4078
4079pages/login/ui/RegisterPage.tsx
4080
4081```tsx
4082import { registrationData } from "../model/registration-schema";
4083
4084
4085function validate(formData: FormData) {
4086 const data = Object.fromEntries(formData.entries());
4087 try {
4088 registrationData.parse(data);
4089 } catch (error) {
4090 // TODO: Show error message to the user
4091 }
4092}
4093
4094
4095export function RegisterPage() {
4096 return (
4097 <form onSubmit={(e) => validate(new FormData(e.target))}>
4098 <label htmlFor="email">E-mail</label>
4099 <input id="email" name="email" required />
4100
4101
4102 <label htmlFor="password">Password (min. 6 characters)</label>
4103 <input id="password" name="password" type="password" required />
4104
4105
4106 <label htmlFor="confirmPassword">Confirm password</label>
4107 <input id="confirmPassword" name="confirmPassword" type="password" required />
4108 </form>
4109 )
4110}
4111```
4112
4113## How to send credentials to the backend
4114
4115[Section titled “How to send credentials to the backend”](#how-to-send-credentials-to-the-backend)
4116
4117Create 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. As explained in the [guide for API requests](/docs/guides/examples/api-requests), you can put your request either in `shared/api` or in the `api` segment of your login page.
4118
4119### Two-factor authentication
4120
4121[Section titled “Two-factor authentication”](#two-factor-authentication)
4122
4123If 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.
4124
4125Since this page is very related to logging in, you can also keep it in the same slice, `login` on the Pages layer.
4126
4127You 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.
4128
4129## How to store the token for authenticated requests
4130
4131[Section titled “How to store the token for authenticated requests”](#how-to-store-the-token-for-authenticated-requests)
4132
4133Regardless 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.
4134
4135The 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, [Remix](https://remix.run)), then you should store the server-side cookie infrastructure in `shared/api`. There is an example in [the Authentication section of the tutorial](/docs/get-started/tutorial#authentication) of how to do that with Remix.
4136
4137Sometimes, 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.
4138
4139### In Shared
4140
4141[Section titled “In Shared”](#in-shared)
4142
4143This 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.
4144
4145Automatic 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:
4146
4147* Authenticate and store the access token as well as the refresh token
4148* Make any request that requires authentication
4149* 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
4150
4151One 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`.
4152
4153Another 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`.
4154
4155### In Entities
4156
4157[Section titled “In Entities”](#in-entities)
4158
4159It’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.
4160
4161Note
4162
4163The **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.
4164
4165To 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.
4166
4167Since 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 [the import rule on layers](/docs/reference/layers#import-rule-on-layers):
4168
4169> A module (file) in a slice can only import other slices when they are located on layers strictly below.
4170
4171There are several solutions to this challenge:
4172
41731. **Pass the token manually every time you make a request**\
4174 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.
41752. **Expose the token to the entire app with a context or a global store like `localStorage`**\
4176 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.
41773. **Inject the token into the API client every time it changes**\
4178 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”).
4179
4180Once 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`.
4181
4182### In Pages/Widgets (not recommended)
4183
4184[Section titled “In Pages/Widgets (not recommended)”](#in-pageswidgets-not-recommended)
4185
4186It 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.
4187
4188## Logout and token invalidation
4189
4190[Section titled “Logout and token invalidation”](#logout-and-token-invalidation)
4191
4192Usually, 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.
4193
4194If 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.
4195
4196The 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.
4197
4198### Automatic logout
4199
4200[Section titled “Automatic logout”](#automatic-logout)
4201
4202Don’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`.
4203
4204# Handling Assets
4205
4206Assets are static resources that your application uses. In FSD, assets follow the same placement principles as code. Group them by use case, not by type, and keep them close to the components that use them.
4207
4208Caution
4209
4210It’s a common practice to implement a custom `assets` segment for all static files, but this pattern is generally discouraged, as it violates FSD principles of high cohesion and locality of changes.
4211
4212If you are interested in a more detailed explanation, you can check articles on [Desegmentation](/docs/guides/issues/desegmented/) and [Slices and segments](/docs/reference/slices-segments/).
4213
4214## Slice-specific Assets
4215
4216[Section titled “Slice-specific Assets”](#slice-specific-assets)
4217
4218When an asset is only used by one page, widget, or feature, keep it inside that slice. This maintains encapsulation and makes it clear where the asset belongs:
4219
4220* pages/
4221
4222 * home/
4223
4224 * ui/
4225
4226 * hero-image.jpg
4227 * HomePage.tsx
4228
4229 * index.ts
4230
4231If your UI has many static assets like images, you can create a subfolder in the `ui` segment:
4232
4233* pages/
4234
4235 * home/
4236
4237 * ui/
4238
4239 * previews/
4240
4241 * cake.jpg
4242 * pizza.jpg
4243 * …
4244
4245 * HomePage.tsx
4246
4247 * index.ts
4248
4249### Non-UI Assets
4250
4251[Section titled “Non-UI Assets”](#non-ui-assets)
4252
4253Sometimes an asset is not part of the UI but is a crucial part of business logic. Consider this example:
4254
4255* features/
4256
4257 * billing/
4258
4259 * model/
4260
4261 * invoice-template.pdf
4262 * create-invoice.ts
4263
4264 * index.ts
4265
4266Here, `invoice-template.pdf` is tightly coupled with the business logic in `create-invoice.ts`. In cases like this, it’s preferable to place the file in the `model` segment to maintain high cohesion and localize changes.
4267
4268## Shared Assets
4269
4270[Section titled “Shared Assets”](#shared-assets)
4271
4272If a static asset is used multiple times, you can move it to `shared/ui`, so it is accessible to every other part of the application.
4273
4274* shared/
4275
4276 * ui/
4277
4278 * placeholders/
4279
4280 * cake.jpg
4281 * pizza.jpg
4282 * …
4283
4284The same approach can be applied for static assets that are used by shared components:
4285
4286* shared/
4287
4288 * ui/
4289
4290 * Dropdown.tsx
4291 * chevron.svg
4292
4293## Global Assets
4294
4295[Section titled “Global Assets”](#global-assets)
4296
4297Global assets include things like global stylesheets (such as a CSS reset) and fonts. The `app` layer is the right place for these files since they’re only used by the application entrypoint.
4298
4299* app/
4300
4301 * styles/
4302
4303 * reset.css
4304 * global.css
4305
4306 * main.ts
4307
4308## Public Folder
4309
4310[Section titled “Public Folder”](#public)
4311
4312Most bundlers and frameworks provide a `public` folder at the root of your project. Files here are served as-is without processing.
4313
4314Some build tools like [Vite](https://vite.dev/config/shared-options#publicdir) allow the public folder location to be changed, while others like [Astro](https://docs.astro.build/en/basics/project-structure/#public) don’t have this option. If your build tool doesn’t support this change, that’s fine. The `public` folder is not part of the FSD structure and won’t cause naming collisions.
4315
4316## Summary
4317
4318[Section titled “Summary”](#summary)
4319
4320| Asset Type | Location |
4321| ------------------------- | ----------------------------------------- |
4322| Slice-specific assets | Inside the slice (e.g., `pages/home/ui/`) |
4323| Reusable icons and images | `shared/ui/` |
4324| Global styles | `app/styles/` |
4325| Fonts | `app/fonts/`, `public/` or `app/public` |
4326| Static files, favicon | `public/` or `app/public` |
4327
4328Follow the FSD principle: place assets where they are used, and move them to `shared` when there’s an actual need for reuse across slices.
4329
4330# Page Layouts
4331
4332This guide examines the abstraction of a *page layout* — when several pages share the same overall structure, and differ only in the main content.
4333
4334Note
4335
4336Is 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!
4337
4338## Simple layout
4339
4340[Section titled “Simple layout”](#simple-layout)
4341
4342The 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:
4343
4344shared/ui/layout/Layout.tsx
4345
4346```tsx
4347import { Link, Outlet } from "react-router-dom";
4348import { useThemeSwitcher } from "./useThemeSwitcher";
4349
4350
4351export function Layout({ siblingPages, headings }) {
4352 const [theme, toggleTheme] = useThemeSwitcher();
4353
4354
4355 return (
4356 <div>
4357 <header>
4358 <nav>
4359 <ul>
4360 <li> <Link to="/">Home</Link> </li>
4361 <li> <Link to="/docs">Docs</Link> </li>
4362 <li> <Link to="/blog">Blog</Link> </li>
4363 </ul>
4364 </nav>
4365 <button onClick={toggleTheme}>{theme}</button>
4366 </header>
4367 <main>
4368 <SiblingPageSidebar siblingPages={siblingPages} />
4369 <Outlet /> {/* This is where the main content goes */}
4370 <HeadingsSidebar headings={headings} />
4371 </main>
4372 <footer>
4373 <ul>
4374 <li>GitHub</li>
4375 <li>Twitter</li>
4376 </ul>
4377 </footer>
4378 </div>
4379 );
4380}
4381```
4382
4383shared/ui/layout/useThemeSwitcher.ts
4384
4385```ts
4386export function useThemeSwitcher() {
4387 const [theme, setTheme] = useState("light");
4388
4389
4390 function toggleTheme() {
4391 setTheme(theme === "light" ? "dark" : "light");
4392 }
4393
4394
4395 useEffect(() => {
4396 document.body.classList.remove("light", "dark");
4397 document.body.classList.add(theme);
4398 }, [theme]);
4399
4400
4401 return [theme, toggleTheme] as const;
4402}
4403```
4404
4405The code of sidebars is left as an exercise for the reader 😉.
4406
4407## Using widgets in the layout
4408
4409[Section titled “Using widgets in the layout”](#using-widgets-in-the-layout)
4410
4411Sometimes you want to include certain business logic in the layout, especially if you’re using deeply nested routes with a router like [React Router](https://reactrouter.com/). Then you can’t store the layout in Shared or in Widgets due to [the import rule on layers](/docs/reference/layers#import-rule-on-layers):
4412
4413> A module in a slice can only import other slices when they are located on layers strictly below.
4414
4415Before 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:
4416
44171. **Write the layout inline on the App layer, where you configure the routing**\
4418 This is great for routers that support nesting, because you can group certain routes and apply the layout only to them.
4419
44202. **Just copy-paste it**\
4421 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.
4422
4423If none of the above are applicable, there are two solutions to include a widget in the layout:
4424
44251. **Use render props or slots**\
4426 Most frameworks allow you to pass a piece of UI externally. In React, it’s called [render props](https://www.patterns.dev/react/render-props-pattern/), in Vue it’s called [slots](https://vuejs.org/guide/components/slots).
44272. **Move the layout to the App layer**\
4428 You can also store your layout on the App layer, for example, in `app/layouts`, and compose any widgets you want.
4429
4430## Further reading
4431
4432[Section titled “Further reading”](#further-reading)
4433
4434* There’s an example of how to build a layout with authentication with React and Remix (equivalent to React Router) in the [tutorial](/docs/get-started/tutorial).
4435
4436# Types
4437
4438This guide concerns data types from typed languages like TypeScript and describes where they fit within FSD.
4439
4440Note
4441
4442Is 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!
4443
4444## Utility types
4445
4446[Section titled “Utility types”](#utility-types)
4447
4448Utility types are types that don’t have much meaning on their own and are usually used with other types. For example:
4449
4450```ts
4451type ArrayValues<T extends readonly unknown[]> = T[number];
4452```
4453
4454To make utility types available across your project, either install a library like [`type-fest`](https://github.com/sindresorhus/type-fest), 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.
4455
4456Don’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:
4457
4458* pages/
4459
4460 * home/
4461
4462 * api/
4463
4464 * ArrayValues.ts utility type
4465 * getMemoryUsageMetrics.ts uses the utility type
4466
4467Caution
4468
4469Resist 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.
4470
4471## Business entities and their cross-references
4472
4473[Section titled “Business entities and their cross-references”](#business-entities-and-their-cross-references)
4474
4475Among 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.
4476
4477Business 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 [Zod](https://zod.dev).
4478
4479For example, if you keep all your requests in Shared, you could do it like this:
4480
4481shared/api/songs.ts
4482
4483```ts
4484import type { Artist } from "./artists";
4485
4486
4487interface Song {
4488 id: number;
4489 title: string;
4490 artists: Array<Artist>;
4491}
4492
4493
4494export function listSongs() {
4495 return fetch('/api/songs').then((res) => res.json() as Promise<Array<Song>>);
4496}
4497```
4498
4499You 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 [the import rule on layers](/docs/reference/layers#import-rule-on-layers):
4500
4501> A module in a slice can only import other slices when they are located on layers strictly below.
4502
4503There are two ways to deal with this issue:
4504
45051. **Parametrize your types**\
4506 You can make your types accept type arguments as slots for connections with other entities, and even impose constraints on those slots. For example:
4507
4508 entities/song/model/song.ts
4509
4510 ```ts
4511 interface Song<ArtistType extends { id: string }> {
4512 id: number;
4513 title: string;
4514 artists: Array<ArtistType>;
4515 }
4516 ```
4517
4518 This works better for some types than others. A simple type like `Cart = { items: Array<Product> }` 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.
4519
45202. **Cross-import (but do it right)**\
4521 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:
4522
4523 * entities/
4524
4525 * song/
4526
4527 * @x/
4528
4529 * artist.ts a public API for the `artist` entity to import from
4530 * playlist.ts a public API for the `playlist` entity to import from
4531
4532 * index.ts regular public API
4533
4534 The contents of a file `📄 entities/song/@x/artist.ts` are similar to `📄 entities/song/index.ts`:
4535
4536 entities/song/@x/artist.ts
4537
4538 ```ts
4539 export type { Song } from "../model/song.ts";
4540 ```
4541
4542 Then the `📄 entities/artist/model/artist.ts` can import `Song` like this:
4543
4544 entities/artist/model/artist.ts
4545
4546 ```ts
4547 import type { Song } from "entities/song/@x/artist";
4548
4549
4550 export interface Artist {
4551 name: string;
4552 songs: Array<Song>;
4553 }
4554 ```
4555
4556 By making explicit connections between entities, we stay on top of inter-dependencies and maintain a decent level of domain separation.
4557
4558## Data transfer objects and mappers
4559
4560[Section titled “Data transfer objects and mappers”](#data-transfer-objects-and-mappers)
4561
4562Data 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.
4563
4564### Where to put DTOs
4565
4566[Section titled “Where to put DTOs”](#where-to-put-dtos)
4567
4568If 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.
4569
4570If you have your request functions in `shared/api`, that’s where the DTOs should be, right next to the function that uses them:
4571
4572shared/api/songs.ts
4573
4574```ts
4575import type { ArtistDTO } from "./artists";
4576
4577
4578interface SongDTO {
4579 id: number;
4580 title: string;
4581 artist_ids: Array<ArtistDTO["id"]>;
4582}
4583
4584
4585export function listSongs() {
4586 return fetch('/api/songs').then((res) => res.json() as Promise<Array<SongDTO>>);
4587}
4588```
4589
4590As mentioned in the previous section, storing your requests and DTOs in Shared comes with the benefit of being able to reference other DTOs.
4591
4592### Where to put mappers
4593
4594[Section titled “Where to put mappers”](#where-to-put-mappers)
4595
4596Mappers 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:
4597
4598shared/api/songs.ts
4599
4600```ts
4601import type { ArtistDTO } from "./artists";
4602
4603
4604interface SongDTO {
4605 id: number;
4606 title: string;
4607 disc_no: number;
4608 artist_ids: Array<ArtistDTO["id"]>;
4609}
4610
4611
4612interface Song {
4613 id: string;
4614 title: string;
4615 /** The full title of the song, including the disc number. */
4616 fullTitle: string;
4617 artistIds: Array<string>;
4618}
4619
4620
4621function adaptSongDTO(dto: SongDTO): Song {
4622 return {
4623 id: String(dto.id),
4624 title: dto.title,
4625 fullTitle: `${dto.disc_no} / ${dto.title}`,
4626 artistIds: dto.artist_ids.map(String),
4627 };
4628}
4629
4630
4631export function listSongs() {
4632 return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));
4633}
4634```
4635
4636If 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:
4637
4638entities/song/api/dto.ts
4639
4640```ts
4641import type { ArtistDTO } from "entities/artist/@x/song";
4642
4643
4644export interface SongDTO {
4645 id: number;
4646 title: string;
4647 disc_no: number;
4648 artist_ids: Array<ArtistDTO["id"]>;
4649}
4650```
4651
4652entities/song/api/mapper.ts
4653
4654```ts
4655import type { SongDTO } from "./dto";
4656
4657
4658export interface Song {
4659 id: string;
4660 title: string;
4661 /** The full title of the song, including the disc number. */
4662 fullTitle: string;
4663 artistIds: Array<string>;
4664}
4665
4666
4667export function adaptSongDTO(dto: SongDTO): Song {
4668 return {
4669 id: String(dto.id),
4670 title: dto.title,
4671 fullTitle: `${dto.disc_no} / ${dto.title}`,
4672 artistIds: dto.artist_ids.map(String),
4673 };
4674}
4675```
4676
4677entities/song/api/listSongs.ts
4678
4679```ts
4680import { adaptSongDTO } from "./mapper";
4681
4682
4683export function listSongs() {
4684 return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO));
4685}
4686```
4687
4688entities/song/model/songs.ts
4689
4690```ts
4691import { createSlice, createEntityAdapter } from "@reduxjs/toolkit";
4692
4693
4694import { listSongs } from "../api/listSongs";
4695
4696
4697export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs);
4698
4699
4700const songAdapter = createEntityAdapter();
4701const songsSlice = createSlice({
4702 name: "songs",
4703 initialState: songAdapter.getInitialState(),
4704 reducers: {},
4705 extraReducers: (builder) => {
4706 builder.addCase(fetchSongs.fulfilled, (state, action) => {
4707 songAdapter.upsertMany(state, action.payload);
4708 })
4709 },
4710});
4711```
4712
4713### How to deal with nested DTOs
4714
4715[Section titled “How to deal with nested DTOs”](#how-to-deal-with-nested-dtos)
4716
4717The 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:
4718
4719entities/song/model/songs.ts
4720
4721```ts
4722import {
4723 createSlice,
4724 createEntityAdapter,
4725 createAsyncThunk,
4726 createSelector,
4727} from '@reduxjs/toolkit'
4728import { normalize, schema } from 'normalizr'
4729
4730
4731import { getSong } from "../api/getSong";
4732
4733
4734// Define normalizr entity schemas
4735export const artistEntity = new schema.Entity('artists')
4736export const songEntity = new schema.Entity('songs', {
4737 artists: [artistEntity],
4738})
4739
4740
4741const songAdapter = createEntityAdapter()
4742
4743
4744export const fetchSong = createAsyncThunk(
4745 'songs/fetchSong',
4746 async (id: string) => {
4747 const data = await getSong(id)
4748 // Normalize the data so reducers can load a predictable payload, like:
4749 // `action.payload = { songs: {}, artists: {} }`
4750 const normalized = normalize(data, songEntity)
4751 return normalized.entities
4752 }
4753)
4754
4755
4756export const slice = createSlice({
4757 name: 'songs',
4758 initialState: songAdapter.getInitialState(),
4759 reducers: {},
4760 extraReducers: (builder) => {
4761 builder.addCase(fetchSong.fulfilled, (state, action) => {
4762 songAdapter.upsertMany(state, action.payload.songs)
4763 })
4764 },
4765})
4766
4767
4768const reducer = slice.reducer
4769export default reducer
4770```
4771
4772entities/song/@x/artist.ts
4773
4774```ts
4775export { fetchSong } from "../model/songs";
4776```
4777
4778entities/artist/model/artists.ts
4779
4780```ts
4781import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
4782
4783
4784import { fetchSong } from 'entities/song/@x/artist'
4785
4786
4787const artistAdapter = createEntityAdapter()
4788
4789
4790export const slice = createSlice({
4791 name: 'users',
4792 initialState: artistAdapter.getInitialState(),
4793 reducers: {},
4794 extraReducers: (builder) => {
4795 builder.addCase(fetchSong.fulfilled, (state, action) => {
4796 // And handle the same fetch result by inserting the artists here
4797 artistAdapter.upsertMany(state, action.payload.artists)
4798 })
4799 },
4800})
4801
4802
4803const reducer = slice.reducer
4804export default reducer
4805```
4806
4807This 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.
4808
4809## Global types and Redux
4810
4811[Section titled “Global types and Redux”](#global-types-and-redux)
4812
4813Global 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:
4814
48151. Generic types that don’t have any application specifics
48162. Types that need to know about the whole application
4817
4818The 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`.
4819
4820Caution
4821
4822Avoid 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.
4823
4824The 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:
4825
4826app/store/index.ts
4827
4828```ts
4829import { combineReducers, rootReducer } from "redux";
4830
4831
4832import { songReducer } from "entities/song";
4833import { artistReducer } from "entities/artist";
4834
4835
4836const rootReducer = combineReducers(songReducer, artistReducer);
4837
4838
4839const store = createStore(rootReducer);
4840
4841
4842type RootState = ReturnType<typeof rootReducer>;
4843type AppDispatch = typeof store.dispatch;
4844```
4845
4846It 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 [import rule on layers](/docs/reference/layers#import-rule-on-layers):
4847
4848> A module in a slice can only import other slices when they are located on layers strictly below.
4849
4850The 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.
4851
4852In TypeScript, you can do it by declaring the types as global like this:
4853
4854app/store/index.ts
4855
4856```ts
4857/* same content as in the code block before… */
4858
4859
4860declare type RootState = ReturnType<typeof rootReducer>;
4861declare type AppDispatch = typeof store.dispatch;
4862```
4863
4864shared/store/index.ts
4865
4866```ts
4867import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux";
4868
4869
4870export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
4871export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
4872```
4873
4874## Enums
4875
4876[Section titled “Enums”](#enums)
4877
4878The 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.
4879
4880The 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.
4881
4882Some 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.).
4883
4884## Type validation schemas and Zod
4885
4886[Section titled “Type validation schemas and Zod”](#type-validation-schemas-and-zod)
4887
4888If 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 [Zod](https://zod.dev). Validation schemas should also be colocated with the code that uses them, as much as possible.
4889
4890Validation schemas are similar to mappers (as discussed in the [Data transfer objects and mappers](#data-transfer-objects-and-mappers) section) in the sense that they take a data transfer object and parse it, producing an error if the parsing fails.
4891
4892One 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.
4893
4894If 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.
4895
4896## Typings of component props and context
4897
4898[Section titled “Typings of component props and context”](#typings-of-component-props-and-context)
4899
4900In 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.
4901
4902Here’s an example with JSX (React or Solid):
4903
4904pages/home/ui/RecentActions.tsx
4905
4906```ts
4907interface RecentActionsProps {
4908 actions: Array<{ id: string; text: string }>;
4909}
4910
4911
4912export function RecentActions({ actions }: RecentActionsProps) {
4913 /* … */
4914}
4915```
4916
4917And here’s an example with the interface stored in a separate file for Vue:
4918
4919pages/home/ui/RecentActionsProps.ts
4920
4921```ts
4922export interface RecentActionsProps {
4923 actions: Array<{ id: string; text: string }>;
4924}
4925```
4926
4927pages/home/ui/RecentActions.vue
4928
4929```html
4930<script setup lang="ts">
4931 import type { RecentActionsProps } from "./RecentActionsProps";
4932
4933
4934 const props = defineProps<RecentActionsProps>();
4935</script>
4936```
4937
4938## Ambient declaration files (`*.d.ts`)
4939
4940[Section titled “Ambient declaration files (\*.d.ts)”](#ambient-declaration-files-dts)
4941
4942Some packages, for example, [Vite](https://vitejs.dev) or [ts-reset](https://www.totaltypescript.com/ts-reset), 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/`.
4943
4944Other 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:
4945
4946shared/lib/untyped-packages/use-react-screenshot.d.ts
4947
4948```ts
4949// This library doesn't have typings, and we didn't want to bother writing our own.
4950declare module "use-react-screenshot";
4951```
4952
4953## Auto-generation of types
4954
4955[Section titled “Auto-generation of types”](#auto-generation-of-types)
4956
4957It’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.
4958
4959# Cross-imports
4960
4961A **cross-import** is an import **between different slices within the same layer**.
4962
4963For example:
4964
4965* importing `features/product` from `features/cart`
4966* importing `widgets/sidebar` from `widgets/header`
4967
4968Cross-imports are a code smell: a warning sign that slices are becoming coupled. In some situations they may be hard to avoid, but they should always be deliberate and either documented or shared within the team/project.
4969
4970Note
4971
4972The `shared` and `app` layers do not have the concept of a slice, so imports *within* those layers are **not** considered cross-imports.
4973
4974## Why is this a code smell?
4975
4976[Section titled “Why is this a code smell?”](#why-is-this-a-code-smell)
4977
4978Cross-imports are not just a matter of style—they are generally considered a **code smell** because they blur the boundaries between domains and introduce implicit dependencies.
4979
4980Consider a case where the `cart` slice directly depends on `product` business logic. At first glance, this might seem convenient. However, this creates several problems:
4981
49821. **Unclear ownership and responsibility.** When `cart` imports from `product`, it becomes unclear which slice “owns” the shared logic. If the `product` team changes their internal implementation, they might unknowingly break `cart`. This ambiguity makes it harder to reason about the codebase and assign responsibility for bugs or features.
4983
49842. **Reduced isolation and testability.** One of the main benefits of sliced architecture is that each slice can be developed, tested, and deployed independently. Cross-imports break this isolation—testing `cart` now requires setting up `product` as well, and changes in one slice can cause unexpected test failures in another.
4985
49863. **Increased cognitive load.** Working on `cart` also requires accounting for how `product` is structured and how it behaves. As cross-imports accumulate, tracing the impact of a change requires following more code across slice boundaries, and even small edits demand more context to be held in mind.
4987
49884. **Path to circular dependencies.** Cross-imports often start as one-way dependencies but can evolve into bidirectional ones (A imports B, B imports A). This tends to lock slices together, making dependencies harder to untangle and increasing refactoring cost over time.
4989
4990The purpose of clear domain boundaries is to keep each slice focused and changeable within its own responsibility. When dependencies are loose, it becomes easier to predict the impact of a change and to keep review and testing scope contained. Cross-imports weaken this separation, expanding the impact of changes and increasing refactoring cost over time—this is why they are treated as a code smell worth addressing.
4991
4992In the sections below, we outline how these issues typically appear in real projects and what strategies you can use to address them.
4993
4994## Entities layer cross-imports
4995
4996[Section titled “Entities layer cross-imports”](#entities-layer-cross-imports)
4997
4998Cross-imports in `entities` are often caused by splitting entities too granularly. Before reaching for `@x`, consider whether the boundaries should be merged instead. Some teams use `@x` as a dedicated cross-import surface for `entities`, but it should be treated as a **last resort** — a **necessary compromise**, not a recommended approach.
4999
5000Think of `@x` as an explicit gateway for unavoidable domain references—not a general-purpose reuse mechanism. Overuse tends to lock entity boundaries together and makes refactoring more costly over time.
5001
5002For details about `@x`, see the [Public API documentation](/docs/reference/public-api).
5003
5004For concrete examples of cross-references between business entities, see:
5005
5006* [Types guide — Business entities and their cross-references](/docs/guides/examples/types#business-entities-and-their-cross-references)
5007* [Layers reference — Entities](/docs/reference/layers#entities)
5008
5009## Features and widgets: Multiple strategies
5010
5011[Section titled “Features and widgets: Multiple strategies”](#features-and-widgets-multiple-strategies)
5012
5013In the `features` and `widgets` layers, it’s usually more realistic to say there are **multiple strategies** for handling cross-imports, rather than declaring them **always forbidden**. This section focuses less on code and more on the **patterns** you can choose from depending on your team and product context.
5014
5015### Strategy A: Slice merge
5016
5017[Section titled “Strategy A: Slice merge”](#strategy-a-slice-merge)
5018
5019If two slices are not truly independent and they are always changed together, merge them into a single larger slice.
5020
5021Example (before):
5022
5023* `features/profile`
5024* `features/profileSettings`
5025
5026If these keep cross-importing each other and effectively move as one unit, they are likely one feature in practice. In that case, merging into `features/profile` is often the simpler and cleaner choice.
5027
5028### Strategy B: Push shared domain flows down into `entities` (domain-only)
5029
5030[Section titled “Strategy B: Push shared domain flows down into entities (domain-only)”](#strategy-b-push-shared-domain-flows-down-into-entities-domain-only)
5031
5032If multiple features share a domain-level flow, move that flow into a domain slice inside `entities` (for example, `entities/session`).
5033
5034Key principles:
5035
5036* `entities` contains **domain types and domain logic only**
5037* UI remains in `features` / `widgets`
5038* features import and use the domain logic from `entities`
5039
5040For example, if both `features/auth` and `features/profile` need session validation, place session-related domain functions in `entities/session` and reuse them from both features.
5041
5042For more guidance, see [Layers reference — Entities](/docs/reference/layers#entities).
5043
5044### Strategy C: Compose from an upper layer (pages / app)
5045
5046[Section titled “Strategy C: Compose from an upper layer (pages / app)”](#strategy-c-compose-from-an-upper-layer-pages--app)
5047
5048Instead of connecting slices within the same layer via cross-imports, compose them at a higher level (`pages` / `app`). This approach uses **Inversion of Control (IoC)** patterns—rather than slices knowing about each other, an upper layer assembles and connects them.
5049
5050Common IoC techniques include:
5051
5052* **Render props (React)**: Pass components or render functions as props
5053* **Slots (Vue)**: Use named slots to inject content from parent components
5054* **Dependency injection**: Pass dependencies through props or context
5055
5056#### Basic composition example (React):
5057
5058[Section titled “Basic composition example (React):”](#basic-composition-example-react)
5059
5060features/userProfile/index.ts
5061
5062```tsx
5063export { UserProfilePanel } from './ui/UserProfilePanel';
5064```
5065
5066features/activityFeed/index.ts
5067
5068```tsx
5069export { ActivityFeed } from './ui/ActivityFeed';
5070```
5071
5072pages/UserDashboardPage.tsx
5073
5074```tsx
5075import React from 'react';
5076import { UserProfilePanel } from '@/features/userProfile';
5077import { ActivityFeed } from '@/features/activityFeed';
5078
5079
5080export function UserDashboardPage() {
5081 return (
5082 <div>
5083 <UserProfilePanel />
5084 <ActivityFeed />
5085 </div>
5086 );
5087}
5088```
5089
5090With this structure, `features/userProfile` and `features/activityFeed` do not know about each other. `pages/UserDashboardPage` composes them to build the full screen.
5091
5092#### Render props example (React):
5093
5094[Section titled “Render props example (React):”](#render-props-example-react)
5095
5096When one feature needs to render content from another, use render props to invert the dependency:
5097
5098features/commentList/ui/CommentList.tsx
5099
5100```tsx
5101interface CommentListProps {
5102 comments: Comment[];
5103 renderUserAvatar?: (userId: string) => React.ReactNode;
5104}
5105
5106
5107export function CommentList({ comments, renderUserAvatar }: CommentListProps) {
5108 return (
5109 <ul>
5110 {comments.map(comment => (
5111 <li key={comment.id}>
5112 {renderUserAvatar?.(comment.userId)}
5113 <span>{comment.text}</span>
5114 </li>
5115 ))}
5116 </ul>
5117 );
5118}
5119```
5120
5121pages/PostPage.tsx
5122
5123```tsx
5124import { CommentList } from '@/features/commentList';
5125import { UserAvatar } from '@/features/userProfile';
5126
5127
5128export function PostPage() {
5129 return (
5130 <CommentList
5131 comments={comments}
5132 renderUserAvatar={(userId) => <UserAvatar userId={userId} />}
5133 />
5134 );
5135}
5136```
5137
5138Now `CommentList` doesn’t import from `userProfile`—the page injects the avatar component.
5139
5140#### Slots example (Vue):
5141
5142[Section titled “Slots example (Vue):”](#slots-example-vue)
5143
5144Vue’s slot system provides a natural way to compose features without cross-imports:
5145
5146features/commentList/ui/CommentList.vue
5147
5148```vue
5149<template>
5150 <ul>
5151 <li v-for="comment in comments" :key="comment.id">
5152 <slot name="avatar" :userId="comment.userId" />
5153 <span>{{ comment.text }}</span>
5154 </li>
5155 </ul>
5156</template>
5157
5158
5159<script setup lang="ts">
5160defineProps<{
5161 comments: Comment[];
5162}>();
5163</script>
5164```
5165
5166pages/PostPage.vue
5167
5168```vue
5169<template>
5170 <CommentList :comments="comments">
5171 <template #avatar="{ userId }">
5172 <UserAvatar :userId="userId" />
5173 </template>
5174 </CommentList>
5175</template>
5176
5177
5178<script setup lang="ts">
5179import { CommentList } from '@/features/commentList';
5180import { UserAvatar } from '@/features/userProfile';
5181</script>
5182```
5183
5184The `CommentList` feature remains independent of `userProfile`. The page uses slots to compose them together.
5185
5186### Strategy D: Cross-feature reuse only via Public API
5187
5188[Section titled “Strategy D: Cross-feature reuse only via Public API”](#strategy-d-cross-feature-reuse-only-via-public-api)
5189
5190If the above strategies don’t fit your case and cross-feature reuse is truly unavoidable, allow it only through an explicit **Public API** (for example: exported hooks or UI components). Avoid directly accessing another slice’s `store`/`model` or internal implementation details.
5191
5192Unlike strategies A-C which aim to eliminate cross-imports, this strategy accepts them while minimizing the risks through strict boundaries.
5193
5194#### Example code:
5195
5196[Section titled “Example code:”](#example-code)
5197
5198features/auth/index.ts
5199
5200```tsx
5201export { useAuth } from './model/useAuth';
5202export { AuthButton } from './ui/AuthButton';
5203```
5204
5205features/profile/ui/ProfileMenu.tsx
5206
5207```tsx
5208import React from 'react';
5209import { useAuth, AuthButton } from '@/features/auth';
5210
5211
5212export function ProfileMenu() {
5213 const { user } = useAuth();
5214
5215
5216 if (!user) {
5217 return <AuthButton />;
5218 }
5219
5220
5221 return <div>{user.name}</div>;
5222}
5223```
5224
5225For example, prevent `features/profile` from importing from paths like `features/auth/model/internal/*`. Restrict usage to only what `features/auth` explicitly exposes as its Public API.
5226
5227## When should cross-imports be treated as a problem?
5228
5229[Section titled “When should cross-imports be treated as a problem?”](#when-should-cross-imports-be-treated-as-a-problem)
5230
5231After reviewing these strategies, a natural question is:
5232
5233> When is a cross-import acceptable to keep, and when should it be treated as a code smell and refactored?
5234
5235Common warning signs:
5236
5237* directly depending on another slice’s store/model/business logic
5238* deep imports into another slice’s internal files
5239* **bidirectional dependencies** (A imports B, and B imports A)
5240* changes in one slice frequently breaking another slice
5241* flows that should be composed in `pages` / `app`, but are forced into cross-imports within the same layer
5242
5243When you see these signals, treat the cross-import as a **code smell** and consider applying at least one of the strategies above.
5244
5245## How strict you are is a team/project decision
5246
5247[Section titled “How strict you are is a team/project decision”](#how-strict-you-are-is-a-teamproject-decision)
5248
5249How strictly to enforce these rules depends on the team and project.
5250
5251For example:
5252
5253* In **early-stage products** with heavy experimentation, allowing some cross-imports may be a pragmatic speed trade-off.
5254* In **long-lived or regulated systems** (for example, fintech or large-scale services), stricter boundaries often pay off in maintainability and stability.
5255
5256Cross-imports are not an absolute prohibition here. They are dependencies that are **generally best avoided**, but sometimes used intentionally.
5257
5258If you do introduce a cross-import:
5259
5260* treat it as a deliberate architectural choice
5261* document the reasoning
5262* revisit it periodically as the system evolves
5263
5264Teams should align on:
5265
5266* what strictness level they want
5267* how to reflect it in lint rules, code review, and documentation
5268* when and how to reevaluate existing cross-imports over time
5269
5270## References
5271
5272[Section titled “References”](#references)
5273
5274* [(Thread) About the supposed inevitability of cross-ports](https://t.me/feature_sliced/4515)
5275* [(Thread) About resolving cross-ports in entities](https://t.me/feature_sliced/3678)
5276* [(Thread) About cross-imports and responsibility](https://t.me/feature_sliced/3287)
5277* [(Thread) About imports between segments](https://t.me/feature_sliced/4021)
5278* [(Thread) About cross-imports inside shared](https://t.me/feature_sliced/3618)
5279
5280# Desegmentation
5281
5282Desegmentation (also known as horizontal slicing or packaging by layer) is a code organization pattern where files are grouped by their technical roles rather than by the business domains they serve. This means code with similar technical functions is stored in the same place, regardless of the business logic it handles.
5283
5284This approach is popular in meta-frameworks like Next and Nuxt due to its simplicity, as it’s easy to get started and enables features like auto-imports and file-based routing:
5285
5286* app
5287
5288 * components
5289
5290 * DeliveryCard.jsx
5291 * DeliveryChoice.jsx
5292 * RegionSelect.jsx
5293 * UserAvatar.jsx
5294
5295 * actions
5296
5297 * delivery.js
5298 * region.js
5299 * user.js
5300
5301 * composables
5302
5303 * delivery.js
5304 * region.js
5305 * user.js
5306
5307 * constants
5308
5309 * delivery.js
5310 * region.js
5311 * user.js
5312
5313 * utils
5314
5315 * delivery.js
5316 * region.js
5317 * user.js
5318
5319 * stores
5320
5321 * delivery
5322
5323 * getters.js
5324 * actions.js
5325
5326This pattern also occurs in FSD codebases, in the form of generic folders:
5327
5328* features
5329
5330 * delivery
5331
5332 * ui
5333
5334 * components ⚠️
5335
5336* entities
5337
5338 * recommendations
5339
5340 * utils ⚠️
5341
5342Files can also be a source of desegmentation. Files like `types.ts` can aggregate multiple domains, complicating navigation and future refactoring, especially in layers like `pages` or `widgets`:
5343
5344* pages
5345
5346 * delivery
5347
5348 * index.ts
5349
5350 * ui
5351
5352 * DeliveryCard.tsx
5353 * DeliveryChoice.tsx
5354 * UserAvatar.tsx
5355
5356 * model
5357
5358 * types.ts ⚠️
5359 * utils.ts ⚠️
5360
5361 * api
5362
5363 * endpoints.ts ⚠️
5364
5365- types.ts
5366
5367 pages/delivery/model/types.ts
5368
5369 ```ts
5370 // ❌ Bad: Mixed business domains in generic file
5371 export interface DeliveryOption {
5372 id: string;
5373 name: string;
5374 price: number;
5375 }
5376
5377
5378 export interface UserInfo {
5379 id: string;
5380 name: string;
5381 avatar: string;
5382 }
5383 ```
5384
5385- utils.ts
5386
5387 pages/delivery/model/utils.ts
5388
5389 ```ts
5390 // ❌ Bad: Mixed business domains in generic file
5391 export function formatDeliveryPrice(price: number) {
5392 return `$${price.toFixed(2)}`;
5393 }
5394
5395
5396 export function getUserInitials(name: string) {
5397 return name.split(' ').map(n => n[0]).join('');
5398 }
5399 ```
5400
5401- endpoints.ts
5402
5403 pages/delivery/api/endpoints.ts
5404
5405 ```ts
5406 // ❌ Bad: Mixed business domains in generic file
5407 export async function fetchDeliveryOptions() { /* ... */ }
5408 export async function fetchUserInfo() { /* ... */ }
5409 ```
5410
5411## The Problem
5412
5413[Section titled “The Problem”](#the-problem)
5414
5415While this structure is easy to start with, it can lead to scalability issues in larger projects:
5416
5417* Low Cohesion: Modifying a single feature often requires editing files in multiple large folders, such as `pages`, `components`, and `stores`.
5418
5419* Tight Coupling: Components can have unexpected dependencies, leading to complex and tangled dependency chains.
5420
5421* Difficult Refactoring: It requires additional effort to manually extract code related to a specific domain.
5422
5423## Solution
5424
5425[Section titled “Solution”](#solution)
5426
5427Group all code that relates to a specific domain in one place.
5428
5429Avoid generic folder names such as `types`, `components`, `utils`, as well as generic file names like `types.ts`, `utils.ts`, or `helpers.ts`. Instead, use names that directly reflect the domain they represent.
5430
5431Avoid generic file names like `types.ts` if possible, especially in slices with multiple domains:
5432
5433* pages
5434
5435 * delivery
5436
5437 * index.tsx
5438
5439 * ui
5440
5441 * DeliveryPage.tsx
5442 * DeliveryCard.tsx
5443 * DeliveryChoice.tsx
5444 * UserInfo.tsx
5445
5446 * model
5447
5448 * delivery.ts
5449 * user.ts
5450
5451## See Also
5452
5453[Section titled “See Also”](#see-also)
5454
5455* [(Article) About Low Coupling and High Cohesion clearly](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/)
5456* [(Article) Low Coupling and High Cohesion. The Law of Demeter](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9)
5457
5458# Excessive Entities
5459
5460The `entities` layer in Feature-Sliced Design is one of the lower layers that’s primarily for business logic. That makes it widely accessible — all layers except for `shared` can access it. However, its global nature means that changes to `entities` can have a widespread impact, requiring careful design to avoid costly refactors.
5461
5462Excessive entities can lead to ambiguity (what code belongs to this layer), coupling, and constant import dilemmas (code scattered across sibling entities).
5463
5464## How to keep `entities` layer clean
5465
5466[Section titled “How to keep entities layer clean”](#how-to-keep-entities-layer-clean)
5467
5468### 0. Consider having no `entities` layer
5469
5470[Section titled “0. Consider having no entities layer”](#0-consider-having-no-entities-layer)
5471
5472You might think that your application won’t be Feature-Sliced if you don’t include this layer, but it is completely fine for the application to have no `entities` layer. It doesn’t break FSD in any way, on the contrary, it simplifies the architecture and keeps the `entities` layer available for future scaling. For example, if your application acts as a thin client, most likely it doesn’t need `entities` layer.
5473
5474What are thick and thin clients?
5475
5476*Thick* vs. *thin client* distinction refers to how the application processes data:
5477
5478* *Thin* clients rely on the backend for most data processing. Client-side business logic is minimal and involves only exchanging data with the backend.
5479* *Thick* clients handle significant client-side business logic, making them suitable candidates for the `entities` layer.
5480
5481Keep in mind that this classification is not strictly binary, and different parts of the same application may act as a “thick” or a “thin” client.
5482
5483### 1. Avoid preemptive slicing
5484
5485[Section titled “1. Avoid preemptive slicing”](#1-avoid-preemptive-slicing)
5486
5487In contrast to previous versions, FSD 2.1 encourages deferred decomposition of slices instead of preemptive, and this approach also extends to `entities` layer. At first, you can place all your code in the `model` segment of your page (widget, feature), and then consider refactoring it later, when business requirements are stable.
5488
5489Remember: the later you move code to the `entities` layer, the less dangerous your potential refactors will be — code in Entities may affect functionality of any slice on higher layers.
5490
5491### 2. Avoid Unnecessary Entities
5492
5493[Section titled “2. Avoid Unnecessary Entities”](#2-avoid-unnecessary-entities)
5494
5495Do not create an entity for every piece of business logic. Instead, leverage types from `shared/api` and place logic in the `model` segment of a current slice. For reusable business logic, use the `model` segment within an entity slice while keeping data definitions in `shared/api`:
5496
5497* entities
5498
5499 * order
5500
5501 * index.ts
5502
5503 * model
5504
5505 * apply-discount.ts Business logic using OrderDto from shared/api
5506
5507* shared
5508
5509 * api
5510
5511 * index.ts
5512
5513 * endpoints
5514
5515 * order.ts
5516
5517### 3. Exclude CRUD Operations from Entities
5518
5519[Section titled “3. Exclude CRUD Operations from Entities”](#3-exclude-crud-operations-from-entities)
5520
5521CRUD operations, while essential, often involve boilerplate code without significant business logic. Including them in the `entities` layer can clutter it and obscure meaningful code. Instead, place CRUD operations in `shared/api`:
5522
5523* shared
5524
5525 * api
5526
5527 * client.ts
5528
5529 * index.ts
5530
5531 * endpoints
5532
5533 * order.ts Contains all order-related CRUD operations
5534 * products.ts
5535 * cart.ts
5536
5537For complex CRUD operations (e.g., atomic updates, rollbacks, or transactions), evaluate whether the `entities` layer is appropriate, but use it with caution.
5538
5539### 4. Store Authentication Data in `shared`
5540
5541[Section titled “4. Store Authentication Data in shared”](#4-store-authentication-data-in-shared)
5542
5543Prefer `shared` layer to creating a `user` entity for authentication data, such as tokens or user DTOs returned from the backend. These are context-specific and unlikely to be reused outside authentication scope:
5544
5545* Authentication responses (e.g., tokens or DTOs) often lack fields needed for broader reuse or vary by context (e.g., private vs. public user profiles).
5546* Using entities for auth data can lead to cross-layer imports (e.g., `entities` into `shared`) or usage of `@x` notation, complicating the architecture.
5547
5548Instead, store authentication-related data in `shared/auth` or `shared/api`:
5549
5550* shared
5551
5552 * auth
5553
5554 * use-auth.ts authenticated user info or token
5555 * index.ts
5556
5557 * api
5558
5559 * client.ts
5560
5561 * index.ts
5562
5563 * endpoints
5564
5565 * order.ts
5566
5567For more details on implementing authentication, see [the Authentication guide](/docs/guides/examples/auth).
5568
5569### 5. Minimize Cross-Imports
5570
5571[Section titled “5. Minimize Cross-Imports”](#5-minimize-cross-imports)
5572
5573FSD permits cross-imports via `@x` notation, but they can introduce technical issues like circular dependencies. To avoid this, design entities within isolated business contexts to eliminate the need for cross-imports:
5574
5575Non-Isolated Business Context (Avoid):
5576
5577* entities
5578
5579 * order
5580
5581 * @x
5582 * model
5583
5584 * order-item
5585
5586 * @x
5587 * model
5588
5589 * order-customer-info
5590
5591 * @x
5592 * model
5593
5594Isolated Business Context (Preferred):
5595
5596* entities
5597
5598 * order-info
5599
5600 * index.ts
5601
5602 * model
5603
5604 * order-info.ts
5605
5606An isolated context encapsulates all related logic (e.g., order items and customer info) within a single module, reducing complexity and preventing external modifications to tightly coupled logic.
5607
5608# From a custom architecture
5609
5610This guide describes an approach that might be helpful when migrating from a custom self-made architecture to Feature-Sliced Design.
5611
5612Here is the folder structure of a typical custom architecture. We will be using it as an example in this guide.\
5613Click on the blue arrow to open the folder.
5614
5615* src/
5616
5617 * actions/
5618
5619 * product/
5620
5621 * …
5622
5623 * order/
5624
5625 * …
5626
5627 * api/
5628
5629 * …
5630
5631 * components/
5632
5633 * …
5634
5635 * containers/
5636
5637 * …
5638
5639 * constants/
5640
5641 * …
5642
5643 * i18n/
5644
5645 * …
5646
5647 * modules/
5648
5649 * …
5650
5651 * helpers/
5652
5653 * …
5654
5655 * routes/
5656
5657 * products.jsx
5658 * products.\[id].jsx
5659
5660 * utils/
5661
5662 * …
5663
5664 * reducers/
5665
5666 * …
5667
5668 * selectors/
5669
5670 * …
5671
5672 * styles/
5673
5674 * …
5675
5676 * App.jsx
5677
5678 * index.jsx
5679
5680## Before you start
5681
5682[Section titled “Before you start”](#before-you-start)
5683
5684The 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.
5685
5686Here are some reasons to consider making the switch:
5687
56881. New team members are complaining that it’s hard to get to a productive level
56892. Making modifications to one part of the code **often** causes another unrelated part to break
56903. Adding new functionality is difficult due to the sheer amount of things you need to think about
5691
5692**Avoid switching to FSD against the will of your teammates**, even if you are the lead.\
5693First, convince your teammates that the benefits outweigh the cost of migration and the cost of learning a new architecture instead of the established one.
5694
5695Also 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.
5696
5697Tip
5698
5699If you need help convincing the project manager that FSD is beneficial, consider some of these points:
5700
57011. Migration to FSD can happen incrementally, so it will not halt the development of new features
57022. A good architecture can significantly decrease the time that a new developer needs to get productive
57033. FSD is a documented architecture, so the team doesn’t have to continuously spend time on maintaining their own documentation
5704
5705***
5706
5707If 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.
5708
5709## Step 1. Divide the code by pages
5710
5711[Section titled “Step 1. Divide the code by pages”](#divide-code-by-pages)
5712
5713Most custom architectures already have a division by pages, however small or large in logic. If you already have `📁 pages`, you may skip this step.
5714
5715If 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:
5716
5717Note
5718
5719For 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.
5720
5721Route file:
5722
5723src/routes/products.\[id].js
5724
5725```js
5726export { ProductPage as default } from "@/pages/product"
5727```
5728
5729Page index file:
5730
5731src/pages/product/index.js
5732
5733```js
5734export { ProductPage } from "./ProductPage.jsx"
5735```
5736
5737Page component file:
5738
5739src/pages/product/ProductPage.jsx
5740
5741```jsx
5742export function ProductPage(props) {
5743 return <div />;
5744}
5745```
5746
5747## Step 2. Separate everything else from the pages
5748
5749[Section titled “Step 2. Separate everything else from the pages”](#separate-everything-else-from-pages)
5750
5751Create 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.
5752
5753Remember that the Shared layer doesn’t have slices, so it’s fine if segments import from each other.
5754
5755You should end up with a file structure like this:
5756
5757* src/
5758
5759 * app/
5760
5761 * routes/
5762
5763 * products.jsx
5764 * products.\[id].jsx
5765
5766 * App.jsx
5767
5768 * index.js
5769
5770 * pages/
5771
5772 * product/
5773
5774 * index.js
5775
5776 * ui/
5777
5778 * ProductPage.jsx
5779
5780 * catalog/
5781
5782 * …
5783
5784 * shared/
5785
5786 * actions/
5787
5788 * …
5789
5790 * api/
5791
5792 * …
5793
5794 * components/
5795
5796 * …
5797
5798 * containers/
5799
5800 * …
5801
5802 * constants/
5803
5804 * …
5805
5806 * i18n/
5807
5808 * …
5809
5810 * modules/
5811
5812 * …
5813
5814 * helpers/
5815
5816 * …
5817
5818 * utils/
5819
5820 * …
5821
5822 * reducers/
5823
5824 * …
5825
5826 * selectors/
5827
5828 * …
5829
5830 * styles/
5831
5832 * …
5833
5834## Step 3. Tackle cross-imports between pages
5835
5836[Section titled “Step 3. Tackle cross-imports between pages”](#tackle-cross-imports-between-pages)
5837
5838Find all instances where one page is importing from the other and do one of the two things:
5839
58401. Copy-paste the imported code into the depending page to remove the dependency
5841
58422. Move the code to a proper segment in Shared:
5843
5844 * if it’s a part of the UI kit, move it to `📁 shared/ui`;
5845 * if it’s a configuration constant, move it to `📁 shared/config`;
5846 * if it’s a backend interaction, move it to `📁 shared/api`.
5847
5848Note
5849
5850**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.
5851
5852However, 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.
5853
5854## Step 4. Unpack the Shared layer
5855
5856[Section titled “Step 4. Unpack the Shared layer”](#unpack-shared-layer)
5857
5858You 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.
5859
5860Find 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.
5861
5862You should end up with a file structure like this:
5863
5864* src/
5865
5866 * app/
5867
5868 * …
5869
5870 * pages/
5871
5872 * product/
5873
5874 * actions/
5875
5876 * …
5877
5878 * reducers/
5879
5880 * …
5881
5882 * selectors/
5883
5884 * …
5885
5886 * ui/
5887
5888 * Component.jsx
5889 * Container.jsx
5890 * ProductPage.jsx
5891
5892 * index.js
5893
5894 * catalog/
5895
5896 * …
5897
5898 * shared/ only objects that are reused
5899
5900 * actions/
5901
5902 * …
5903
5904 * api/
5905
5906 * …
5907
5908 * components/
5909
5910 * …
5911
5912 * containers/
5913
5914 * …
5915
5916 * constants/
5917
5918 * …
5919
5920 * i18n/
5921
5922 * …
5923
5924 * modules/
5925
5926 * …
5927
5928 * helpers/
5929
5930 * …
5931
5932 * utils/
5933
5934 * …
5935
5936 * reducers/
5937
5938 * …
5939
5940 * selectors/
5941
5942 * …
5943
5944 * styles/
5945
5946 * …
5947
5948## Step 5. Organize code by technical purpose
5949
5950[Section titled “Step 5. Organize code by technical purpose”](#organize-by-technical-purpose)
5951
5952In FSD, division by technical purpose is done with *segments*. There are a few common ones:
5953
5954* `ui` — everything related to UI display: UI components, date formatters, styles, etc.
5955* `api` — backend interactions: request functions, data types, mappers, etc.
5956* `model` — the data model: schemas, interfaces, stores, and business logic.
5957* `lib` — library code that other modules on this slice need.
5958* `config` — configuration files and feature flags.
5959
5960You 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.
5961
5962Reorganize 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.
5963
5964Also reorganize the Shared layer to remove these folders:
5965
5966* `📁 components`, `📁 containers` — most of it should become `📁 shared/ui`;
5967* `📁 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`;
5968* `📁 constants` — again, group by function and move to `📁 shared/config`.
5969
5970## Optional steps
5971
5972[Section titled “Optional steps”](#optional-steps)
5973
5974### Step 6. Form entities/features from Redux slices that are used on several pages
5975
5976[Section titled “Step 6. Form entities/features from Redux slices that are used on several pages”](#form-entities-features-from-redux)
5977
5978Usually, 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.
5979
5980Entities and features are meant to be independent from each other. If your business domain contains inherent connections between entities, refer to the [guide on business entities](/docs/guides/examples/types#business-entities-and-their-cross-references) for advice on how to organize these connections.
5981
5982The API functions related to these slices can stay in `📁 shared/api`.
5983
5984### Step 7. Refactor your modules
5985
5986[Section titled “Step 7. Refactor your modules”](#refactor-your-modules)
5987
5988The `📁 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.
5989
5990### Step 8. Form a clean UI foundation in `shared/ui`
5991
5992[Section titled “Step 8. Form a clean UI foundation in shared/ui”](#form-clean-ui-foundation)
5993
5994`📁 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.
5995
5996Refactor 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.
5997
5998## See also
5999
6000[Section titled “See also”](#see-also)
6001
6002* [(Talk in Russian) Ilya Klimov — Крысиные бега бесконечного рефакторинга: как не дать техническому долгу убить мотивацию и продукт](https://youtu.be/aOiJ3k2UvO4)
6003
6004# Migration from v1 to v2
6005
6006## Why v2?
6007
6008[Section titled “Why v2?”](#why-v2)
6009
6010The original concept of **feature-slices** [was announced](https://t.me/feature_slices) in 2018.
6011
6012Since then, many transformations of the methodology have taken place, but at the same time **[the basic principles were preserved](https://feature-sliced.github.io/featureslices.dev/v1.0.html)**:
6013
6014* Using a *standardized* frontend project structure
6015* Splitting the application in the first place-according to *business logic*
6016* Use of *isolated features* to prevent implicit side effects and cyclic dependencies
6017* Using the *Public API* with a ban on climbing “into the insides” of the module
6018
6019At the same time, in the previous version of the methodology, there were still **weak points** that
6020
6021* Sometimes it leads to boilerplate code
6022* Sometimes it leads to excessive complication of the code base and non-obvious rules between abstractions
6023* Sometimes it leads to implicit architectural solutions, which prevented the project from being pulled up and new people from onboarding
6024
6025The new version of the methodology ([v2](https://github.com/feature-sliced/documentation)) is designed **to eliminate these shortcomings, while preserving the existing advantages** of the approach.
6026
6027Since 2018, [has also developed](https://github.com/kof/feature-driven-architecture/issues) another similar methodology - [**feature-driven**](https://github.com/feature-sliced/documentation/tree/rc/feature-driven), which was first announced by [Oleg Isonen](https://github.com/kof).
6028
6029After merging of the two approaches, we have **improved and refined existing practices** - towards greater flexibility, clarity and efficiency in application.
6030
6031> As a result, this has even affected the name of the methodology - *“feature-slice**d**”*
6032
6033## Why does it make sense to migrate the project to v2?
6034
6035[Section titled “Why does it make sense to migrate the project to v2?”](#why-does-it-make-sense-to-migrate-the-project-to-v2)
6036
6037Caution
6038
6039The current version of the methodology is under development and some details *may change*
6040
6041#### 🔍 More transparent and simple architecture
6042
6043[Section titled “🔍 More transparent and simple architecture”](#-more-transparent-and-simple-architecture)
6044
6045The methodology (v2) offers **more intuitive and more common abstractions and ways of separating logic among developers.**
6046
6047All 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.
6048
6049#### 📦 More flexible and honest modularity
6050
6051[Section titled “📦 More flexible and honest modularity”](#-more-flexible-and-honest-modularity)
6052
6053The methodology (v2) allows **to distribute logic in a more flexible way:**
6054
6055* With the ability to refactor isolated parts from scratch
6056* With the ability to rely on the same abstractions, but without unnecessary interweaving of dependencies
6057* With simpler requirements for the location of the new module *(layer => slice => segment)*
6058
6059#### 🚀 More specifications, plans, community
6060
6061[Section titled “🚀 More specifications, plans, community”](#-more-specifications-plans-community)
6062
6063At the moment, the `core-team` is actively working on the latest (v2) version of the methodology
6064
6065So it is for her:
6066
6067* there will be more described cases / problems
6068* there will be more guides on the application
6069* there will be more real examples
6070* in general, there will be more documentation for onboarding new people and studying the concepts of the methodology
6071* the toolkit will be developed in the future to comply with the concepts and conventions on architecture
6072
6073> Of course, there will be user support for the first version as well - but the latest version is still a priority for us
6074>
6075> 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**
6076
6077## Changelog
6078
6079[Section titled “Changelog”](#changelog)
6080
6081### `BREAKING` Layers
6082
6083[Section titled “BREAKING Layers”](#breaking-layers)
6084
6085Now the methodology assumes explicit allocation of layers at the top level
6086
6087* `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared`
6088
6089* *That is, not everything is now treated as features/pages*
6090
6091* This approach allows you to [explicitly set rules for layers](https://t.me/atomicdesign/18708):
6092
6093* The **higher the layer** of the module is located , the more **context** it has
6094
6095 *(in other words-each module of the layer - can import only the modules of the underlying layers, but not higher)*
6096
6097* The **lower the layer of the** module is located , the more **danger and responsibility** to make changes to it
6098
6099 *(because it is usually the underlying layers that are more overused)*
6100
6101### `BREAKING` Shared
6102
6103[Section titled “BREAKING Shared”](#breaking-shared)
6104
6105The 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`
6106
6107* `shared/ui` - Still the same general uikit of the application (optional)
6108 * *At the same time, no one forbids using `Atomic Design` here as before*
6109* `shared/lib` - A set of auxiliary libraries for implementing logic
6110 * *Still - without a dump of helpers*
6111* `shared/api` - A common entry point for accessing the API
6112 * *Can also be registered locally in each feature / page - but it is not recommended*
6113* As before - there should be no explicit binding to business logic in `shared`
6114 * *If necessary, you need to take this relationship to the `entities` level or even higher*
6115
6116### `NEW` Entities, Processes
6117
6118[Section titled “NEW Entities, Processes”](#new-entities-processes)
6119
6120In v2 **, other new abstractions** have been added to eliminate the problems of logic complexity and high coupling.
6121
6122* `/entities` - layer **business entities** containing slices that are related directly to the business models or synthetic entities required only on frontend
6123 * *Examples: `user`, `i18n`, `order`, `blog`*
6124
6125* `/processes` - layer **business processes**, penetrating app
6126
6127 * **The layer is optional**, it is usually recommended to use it when *the logic grows and begins to blur in several pages*
6128 * *Examples: `payment`, `auth`, `quick-tour`*
6129
6130### `BREAKING` Abstractions & Naming
6131
6132[Section titled “BREAKING Abstractions & Naming”](#breaking-abstractions--naming)
6133
6134Now specific abstractions and [clear recommendations for naming them](/docs/about/understanding/naming)are defined
6135
6136#### Layers
6137
6138[Section titled “Layers”](#layers)
6139
6140* `/app` — **application initialization layer**
6141 * *Previous versions: `app`, `core`,`init`, `src/index` (and this happens)*
6142* `/processes` — [**business process layer**](https://github.com/feature-sliced/documentation/discussions/20)
6143 * *Previous versions: `processes`, `flows`, `workflows`*
6144* `/pages` — **application page layer**
6145 * *Previous versions: `pages`, `screens`, `views`, `layouts`, `components`, `containers`*
6146* `/features` — [**functionality parts layer**](https://github.com/feature-sliced/documentation/discussions/23)
6147 * *Previous versions: `features`, `components`, `containers`*
6148* `/entities` — [**business entity layer**](https://github.com/feature-sliced/documentation/discussions/18#discussioncomment-422649)
6149 * *Previous versions: `entities`, `models`, `shared`*
6150* `/shared` — [**layer of reused infrastructure code**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453020) 🔥
6151 * *Previous versions: `shared`, `common`, `lib`*
6152
6153#### Segments
6154
6155[Section titled “Segments”](#segments)
6156
6157* `/ui` — [**UI segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453132) 🔥
6158 * *Previous versions: `ui`, `components`, `view`*
6159* `/model` — [**BL-segment**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-472645) 🔥
6160 * *Previous versions: `model`, `store`, `state`, `services`, `controller`*
6161* `/lib` — segment **of auxiliary code**
6162 * *Previous versions: `lib`, `libs`, `utils`, `helpers`*
6163* `/api` — [**API segment**](https://github.com/feature-sliced/documentation/discussions/66)
6164 * *Previous versions: `api`, `service`, `requests`, `queries`*
6165* `/config` — **application configuration segment**
6166 * *Previous versions: `config`, `env`, `get-env`*
6167
6168### `REFINED` Low coupling
6169
6170[Section titled “REFINED Low coupling”](#refined-low-coupling)
6171
6172Now it is much easier to [observe the principle of low coupling](/docs/reference/slices-segments#zero-coupling-high-cohesion) between modules, thanks to the new layers.
6173
6174*At the same time, it is still recommended to avoid as much as possible cases where it is extremely difficult to “uncouple” modules*
6175
6176## See also
6177
6178[Section titled “See also”](#see-also)
6179
6180* [Notes from the report “React SPB Meetup #1”](https://t.me/feature_slices)
6181* [React Berlin Talk - Oleg Isonen “Feature Driven Architecture”](https://www.youtube.com/watch?v=BWAeYuWFHhs)
6182* [Comparison with v1 (community-chat)](https://t.me/feature_sliced/493)
6183* [New ideas v2 with explanations (atomicdesign-chat)](https://t.me/atomicdesign/18708)
6184* [Discussion of abstractions and naming for the new version of the methodology (v2)](https://github.com/feature-sliced/documentation/discussions/31)
6185
6186# Migration from v2.0 to v2.1
6187
6188The main change in v2.1 is the new mental model for decomposing an interface — pages first.
6189
6190In 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.
6191
6192In 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.
6193
6194Another addition to Feature-Sliced Design is the standardization of cross-imports between entities with the `@x`-notation.
6195
6196## How to migrate
6197
6198[Section titled “How to migrate”](#how-to-migrate)
6199
6200There 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.
6201
6202### Merge slices
6203
6204[Section titled “Merge slices”](#merge-slices)
6205
6206A simple way to start is by running our linter, [Steiger](https://github.com/feature-sliced/steiger), on the project. Steiger is built with the new mental model, and the most helpful rules will be:
6207
6208* [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice) — if an entity or feature is only used in one page, this rule will suggest merging that entity or feature into the page entirely.
6209* [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing) — 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.
6210
6211```bash
6212npx steiger src
6213```
6214
6215This 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.
6216
6217### Standardize cross-imports
6218
6219[Section titled “Standardize cross-imports”](#standardize-cross-imports)
6220
6221If 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:
6222
6223entities/B/some/file.ts
6224
6225```ts
6226import type { EntityA } from "entities/A/@x/B";
6227```
6228
6229For more details, check out the [Public API for cross-imports](/docs/reference/public-api#public-api-for-cross-imports) section in the reference.
6230
6231# Usage with Astro
6232
6233## File-based routing
6234
6235[Section titled “File-based routing”](#file-based-routing)
6236
6237Astro expects routes in `📁 src/pages`, but FSD uses it as a layer for page slices. We can’t use the same folder for both. To fix this, use `📁 src/_pages` as an FSD `🥞 pages` layer, and use `📁 src/pages` only for routing.
6238
6239* src
6240
6241 * pages Astro routing (thin entry points)
6242
6243 * 404.astro built-in 404 error page
6244 * index.astro
6245
6246 * \_pages FSD pages layer
6247
6248 * home
6249
6250 * ui
6251
6252 * HomePage.astro
6253
6254 * index.ts
6255
6256The route file only imports and renders the page from the FSD layer.
6257
6258src/pages/index.astro
6259
6260```astro
6261---
6262import { HomePage } from '@/_pages/home';
6263---
6264
6265
6266<HomePage />
6267```
6268
6269## Setting up path aliases
6270
6271[Section titled “Setting up path aliases”](#setting-up-path-aliases)
6272
6273Add a path alias in `tsconfig.json` to import from `src` using `@/`:
6274
6275tsconfig.json
6276
6277```json
6278{
6279 "extends": "astro/tsconfigs/strict",
6280 "compilerOptions": {
6281 "paths": {
6282 "@/*": ["./src/*"]
6283 }
6284 }
6285}
6286```
6287
6288## Working with integrations
6289
6290[Section titled “Working with integrations”](#working-with-integrations)
6291
6292Some Astro integrations like [Starlight](https://starlight.astro.build/) use content collections for their content. These integrations often expect content in specific folders like `📁 src/content/docs`.
6293
6294If the integration doesn’t let you change the root path, that’s fine — keep it as is. Usually content collection folders don’t conflict with FSD layers so FSD layers (`🥞 _pages`, `🥞 shared`, etc.) can live alongside these folders:
6295
6296* src
6297
6298 * \_pages FSD pages layer
6299
6300 * …
6301
6302 * content Integration content (Starlight docs, etc.)
6303
6304 * docs
6305
6306 * getting-started.md
6307
6308 * shared FSD shared layer
6309
6310 * …
6311
6312Let the integration handle its own routing and rendering, while your FSD structure manages the application-specific code.
6313
6314## See also
6315
6316[Section titled “See also”](#see-also)
6317
6318* [Astro Routing](https://docs.astro.build/en/guides/routing/)
6319* [Astro Project Structure](https://docs.astro.build/en/basics/project-structure/)
6320
6321# Usage with Electron
6322
6323Electron applications have a special architecture consisting of multiple processes with different responsibilities. Applying FSD in such a context requires adapting the structure to the Electron specifics.
6324
6325* src
6326
6327 * app Common app layer
6328
6329 * main Main process
6330
6331 * index.ts Main process entry point
6332
6333 * preload Preload script and Context Bridge
6334
6335 * index.ts Preload entry point
6336
6337 * renderer Renderer process
6338
6339 * index.html Renderer process entry point
6340
6341 * main
6342
6343 * features
6344
6345 * user
6346
6347 * ipc
6348
6349 * get-user.ts
6350 * send-user.ts
6351
6352 * entities
6353
6354 * shared
6355
6356 * renderer
6357
6358 * pages
6359
6360 * settings
6361
6362 * ipc
6363
6364 * get-user.ts
6365 * save-user.ts
6366
6367 * ui
6368
6369 * user.tsx
6370
6371 * index.ts
6372
6373 * home
6374
6375 * ui
6376
6377 * home.tsx
6378
6379 * index.ts
6380
6381 * widgets
6382
6383 * features
6384
6385 * entities
6386
6387 * shared
6388
6389 * shared Common code between main and renderer
6390
6391 * ipc IPC description (event names, contracts)
6392
6393## Public API rules
6394
6395[Section titled “Public API rules”](#public-api-rules)
6396
6397Each process must have its own public API. For example, you can’t import modules from `main` to `renderer`. Only the `src/shared` folder is public for both processes. It’s also necessary for describing contracts for process interaction.
6398
6399## Additional changes to the standard structure
6400
6401[Section titled “Additional changes to the standard structure”](#additional-changes-to-the-standard-structure)
6402
6403It’s suggested to use a new `ipc` segment, where interaction between processes takes place. The `pages` and `widgets` layers, based on their names, should not be present in `src/main`. You can use `features`, `entities` and `shared`. The `app` layer in `src` contains entry points for `main` and `renderer`, as well as the IPC. It’s not desirable for segments in the `app` layer to have intersection points
6404
6405## Interaction example
6406
6407[Section titled “Interaction example”](#interaction-example)
6408
6409src/shared/ipc/channels.ts
6410
6411```typescript
6412export const CHANNELS = {
6413 GET_USER_DATA: 'GET_USER_DATA',
6414 SAVE_USER: 'SAVE_USER',
6415} as const;
6416
6417
6418export type TChannelKeys = keyof typeof CHANNELS;
6419```
6420
6421src/shared/ipc/events.ts
6422
6423```typescript
6424import { CHANNELS } from './channels';
6425
6426
6427export interface IEvents {
6428 [CHANNELS.GET_USER_DATA]: {
6429 args: void,
6430 response?: { name: string; email: string; };
6431 };
6432 [CHANNELS.SAVE_USER]: {
6433 args: { name: string; };
6434 response: void;
6435 };
6436}
6437```
6438
6439src/shared/ipc/preload.ts
6440
6441```typescript
6442import { CHANNELS } from './channels';
6443import type { IEvents } from './events';
6444
6445
6446type TOptionalArgs<T> = T extends void ? [] : [args: T];
6447
6448
6449export type TElectronAPI = {
6450 [K in keyof typeof CHANNELS]: (...args: TOptionalArgs<IEvents[typeof CHANNELS[K]]['args']>) => IEvents[typeof CHANNELS[K]]['response'];
6451};
6452```
6453
6454src/app/preload/index.ts
6455
6456```typescript
6457import { contextBridge, ipcRenderer } from 'electron';
6458import { CHANNELS, type TElectronAPI } from 'shared/ipc';
6459
6460
6461const API: TElectronAPI = {
6462 [CHANNELS.GET_USER_DATA]: () => ipcRenderer.sendSync(CHANNELS.GET_USER_DATA),
6463 [CHANNELS.SAVE_USER]: args => ipcRenderer.invoke(CHANNELS.SAVE_USER, args),
6464} as const;
6465
6466
6467contextBridge.exposeInMainWorld('electron', API);
6468```
6469
6470src/main/features/user/ipc/send-user.ts
6471
6472```typescript
6473import { ipcMain } from 'electron';
6474import { CHANNELS } from 'shared/ipc';
6475
6476
6477export const sendUser = () => {
6478 ipcMain.on(CHANNELS.GET_USER_DATA, ev => {
6479 ev.returnValue = {
6480 name: 'John Doe',
6481 email: 'john.doe@example.com',
6482 };
6483 });
6484};
6485```
6486
6487src/renderer/pages/user-settings/ipc/get-user.ts
6488
6489```typescript
6490import { CHANNELS } from 'shared/ipc';
6491
6492
6493export const getUser = () => {
6494 const user = window.electron[CHANNELS.GET_USER_DATA]();
6495
6496
6497 return user ?? { name: 'John Donte', email: 'john.donte@example.com' };
6498};
6499```
6500
6501## See also
6502
6503[Section titled “See also”](#see-also)
6504
6505* [Process Model Documentation](https://www.electronjs.org/docs/latest/tutorial/process-model)
6506* [Context Isolation Documentation](https://www.electronjs.org/docs/latest/tutorial/context-isolation)
6507* [Inter-Process Communication Documentation](https://www.electronjs.org/docs/latest/tutorial/ipc)
6508* [Example](https://github.com/feature-sliced/examples/tree/master/examples/electron)
6509
6510# Usage with Next.js
6511
6512Caution
6513
6514To avoid conflicts, rename **both** `app` and `pages` FSD layers to `_app` and `_pages`, regardless of which router you use.
6515
6516## src Folder
6517
6518[Section titled “src Folder”](#src-folder)
6519
6520Next.js expects special `app` or `pages` folders either in the root of the project or in the `src` folder. Generally, it is easier to place Next.js folders in the root of the project so that the `src` folder contains only FSD code, but it is not mandatory.
6521
6522## App Router
6523
6524[Section titled “App Router”](#app-router)
6525
6526Next.js uses the `app` folder for the App Router and the `pages` folder for the Pages Router, which conflicts with FSD layer names. To solve this conflict, use prefixed names for FSD layers, such as `_app` instead of `app` and `_pages` instead of `pages`. This approach is also compatible with the official [linter](https://github.com/feature-sliced/steiger).
6527
6528* app Next.js app folder
6529
6530 * api/
6531
6532 * get-example/
6533
6534 * route.ts
6535
6536 * example/
6537
6538 * page.tsx
6539
6540* src/
6541
6542 * \_app/ FSD layer
6543
6544 * api-routes/ API routes
6545
6546 * …
6547
6548 * \_pages/ FSD layer
6549
6550 * example/
6551
6552 * index.ts
6553
6554 * ui/
6555
6556 * example.tsx
6557
6558 * widgets/
6559
6560 * …
6561
6562 * features/
6563
6564 * …
6565
6566 * entities/
6567
6568 * …
6569
6570 * shared/
6571
6572 * …
6573
6574Example of re-exporting a page from `src/_pages` in the Next.js `app`:
6575
6576app/example/page.tsx
6577
6578```tsx
6579export { ExamplePage as default, metadata } from '@/_pages/example';
6580```
6581
6582### Server and client public APIs
6583
6584[Section titled “Server and client public APIs”](#server-and-client-public-apis)
6585
6586In Next.js App Router, modules that can be used on the client and server-only modules may exist together within a single slice. If a server-only module is exported from `index.ts`, server-only side effects can propagate into the client module graph when a Client Component imports that slice, which can lead to build errors.
6587
6588When this problem occurs, add `index.server.ts` to the public API.
6589
6590* `index.server.ts`: Modules that must only be imported on the server, such as Server Components or data access functions marked with `server-only`
6591
6592### Middleware
6593
6594[Section titled “Middleware”](#middleware)
6595
6596If you use middleware in your project, it must be located in the project root alongside the Next.js `app` and `pages` folders.
6597
6598### Instrumentation
6599
6600[Section titled “Instrumentation”](#instrumentation)
6601
6602The `instrumentation.js` file allows you to monitor the performance and behavior of your application. If you use it, it must be located in the project root, similar to `middleware.js`.
6603
6604## Pages Router
6605
6606[Section titled “Pages Router”](#pages-router)
6607
6608### Conflict between FSD and Next.js in the `pages` layer
6609
6610[Section titled “Conflict between FSD and Next.js in the pages layer”](#conflict-between-fsd-and-nextjs-in-the-pages-layer)
6611
6612Routes should be placed in the `pages` folder in the root of the project, similar to `app` folder for the App Router. The structure inside `src` where the layer folders are located remains unchanged.
6613
6614* pages/ Pages folder (Next.js)
6615
6616 * \_app.tsx
6617
6618 * api/
6619
6620 * example.ts API route re-export
6621
6622 * example/
6623
6624 * index.tsx
6625
6626* src/
6627
6628 * \_app/ FSD layer
6629
6630 * custom-app/
6631
6632 * custom-app.tsx Custom App component
6633
6634 * api-routes/
6635
6636 * get-example-data.ts API route
6637
6638 * \_pages/ FSD layer
6639
6640 * example/
6641
6642 * index.ts
6643
6644 * ui/
6645
6646 * example.tsx
6647
6648 * widgets/
6649
6650 * …
6651
6652 * features/
6653
6654 * …
6655
6656 * entities/
6657
6658 * …
6659
6660 * shared/
6661
6662 * …
6663
6664Example of re-exporting a page from `src/_pages` in the Next.js `pages`:
6665
6666pages/example/index.tsx
6667
6668```tsx
6669export { Example as default } from '@/_pages/example';
6670```
6671
6672### Custom `_app` component
6673
6674[Section titled “Custom \_app component”](#custom-_app-component)
6675
6676You can place your Custom App component in `src/_app/_app` or `src/_app/custom-app`:
6677
6678src/\_app/custom-app/custom-app.tsx
6679
6680```tsx
6681import type { AppProps } from 'next/app';
6682
6683
6684export const MyApp = ({ Component, pageProps }: AppProps) => {
6685 return (
6686 <>
6687 <p>My Custom App component</p>
6688 <Component { ...pageProps } />
6689 </>
6690 );
6691};
6692```
6693
6694pages/\_app.tsx
6695
6696```tsx
6697export { App as default } from '@/_app/custom-app';
6698```
6699
6700## Route Handlers (API routes)
6701
6702[Section titled “Route Handlers (API routes)”](#route-handlers-api-routes)
6703
6704Use the `api-routes` segment in the `_app` layer to work with Route Handlers.
6705
6706Be mindful when writing backend code in the FSD structure — FSD is primarily intended for frontends, meaning that’s what people will expect to find. If you need a lot of endpoints, consider separating them into a different package in a monorepo.
6707
6708* App Router
6709
6710 src/\_app/api-routes/get-example-data.ts
6711
6712 ```tsx
6713 import { getExamplesList } from '@/shared/db';
6714
6715
6716 export const getExampleData = () => {
6717 try {
6718 const examplesList = getExamplesList();
6719
6720
6721 return Response.json({ examplesList });
6722 } catch {
6723 return Response.json(null, {
6724 status: 500,
6725 statusText: 'Ouch, something went wrong',
6726 });
6727 }
6728 };
6729 ```
6730
6731 app/api/example/route.ts
6732
6733 ```tsx
6734 export { getExampleData as GET } from '@/_app/api-routes';
6735 ```
6736
6737* Pages Router
6738
6739 src/\_app/api-routes/get-example-data.ts
6740
6741 ```tsx
6742 import type { NextApiRequest, NextApiResponse } from 'next';
6743
6744
6745 const config = {
6746 api: {
6747 bodyParser: {
6748 sizeLimit: '1mb',
6749 },
6750 },
6751 maxDuration: 5,
6752 };
6753
6754
6755 const handler = (req: NextApiRequest, res: NextApiResponse<ResponseData>) => {
6756 res.status(200).json({ message: 'Hello from FSD' });
6757 };
6758
6759
6760 export const getExampleData = { config, handler } as const;
6761 ```
6762
6763 src/\_app/api-routes/index.ts
6764
6765 ```tsx
6766 export { getExampleData } from './get-example-data';
6767 ```
6768
6769 app/api/example.ts
6770
6771 ```tsx
6772 import { getExampleData } from '@/_app/api-routes';
6773
6774
6775 export const config = getExampleData.config;
6776 export default getExampleData.handler;
6777 ```
6778
6779## Additional recommendations
6780
6781[Section titled “Additional recommendations”](#additional-recommendations)
6782
6783* Use the `db` segment in the `shared` layer to describe database queries and their further use in higher layers.
6784* Caching and revalidating queries logic is better kept in the same place as the queries themselves.
6785
6786## See also
6787
6788[Section titled “See also”](#see-also)
6789
6790* [Next.js Project Structure](https://nextjs.org/docs/app/getting-started/project-structure)
6791* [Next.js Page Layouts](https://nextjs.org/docs/app/getting-started/layouts-and-pages)
6792
6793# Usage with NuxtJS
6794
6795It is possible to implement FSD in a NuxtJS project, but conflicts arise due to the differences between NuxtJS project structure requirements and FSD principles:
6796
6797* Initially, NuxtJS offers a project file structure without a `src` folder, i.e. in the root of the project.
6798* The file routing is in the `pages` folder, while in FSD this folder is reserved for the flat slice structure.
6799
6800## Adding an alias for the `src` directory
6801
6802[Section titled “Adding an alias for the src directory”](#adding-an-alias-for-the-src-directory)
6803
6804Add an `alias` object to your config:
6805
6806nuxt.config.ts
6807
6808```ts
6809export default defineNuxtConfig({
6810 devtools: { enabled: true }, // Not FSD related, enabled at project startup
6811 alias: {
6812 "@": '../src'
6813 },
6814})
6815```
6816
6817## Choose how to configure the router
6818
6819[Section titled “Choose how to configure the router”](#choose-how-to-configure-the-router)
6820
6821In 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.
6822
6823### Routing using config
6824
6825[Section titled “Routing using config”](#routing-using-config)
6826
6827In the `app` layer, create a `router.options.ts` file, and export a config object from it:
6828
6829app/router.options.ts
6830
6831```ts
6832import type { RouterConfig } from '@nuxt/schema';
6833
6834
6835export default <RouterConfig> {
6836 routes: (_routes) => [],
6837};
6838```
6839
6840To add a `Home` page to your project, you need to do the following steps:
6841
6842* Add a page slice inside the `pages` layer
6843* Add the appropriate route to the `app/router.config.ts` config
6844
6845To create a page slice, let’s use the [CLI](https://github.com/feature-sliced/cli):
6846
6847```shell
6848fsd pages home
6849```
6850
6851Create a `home-page.vue` file inside the ui segment, access it using the Public API
6852
6853src/pages/home/index.ts
6854
6855```ts
6856export { default as HomePage } from './ui/home-page';
6857```
6858
6859Thus, the file structure will look like this:
6860
6861* src
6862
6863 * app
6864
6865 * router.config.ts
6866
6867 * pages
6868
6869 * home
6870
6871 * ui
6872
6873 * home-page.vue
6874
6875 * index.ts
6876
6877Finally, let’s add a route to the config:
6878
6879app/router.config.ts
6880
6881```ts
6882import type { RouterConfig } from '@nuxt/schema'
6883
6884
6885export default <RouterConfig> {
6886 routes: (_routes) => [
6887 {
6888 name: 'home',
6889 path: '/',
6890 component: () => import('@/pages/home.vue').then(r => r.default || r)
6891 }
6892 ],
6893}
6894```
6895
6896### File Routing
6897
6898[Section titled “File Routing”](#file-routing)
6899
6900First 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:
6901
6902* src
6903
6904 * app
6905
6906 * routes
6907
6908 * pages Pages folder, related to FSD
6909
6910In order for NuxtJS to use the routes folder inside the `app` layer for file routing, you need to modify `nuxt.config.ts` as follows:
6911
6912nuxt.config.ts
6913
6914```ts
6915export default defineNuxtConfig({
6916 devtools: { enabled: true }, // Not FSD related, enabled at project startup
6917 alias: {
6918 "@": '../src'
6919 },
6920 dir: {
6921 pages: './src/app/routes'
6922 }
6923})
6924```
6925
6926Now, you can create routes for pages within `app` and connect pages from `pages` to them.
6927
6928For example, to add a `Home` page to your project, you need to do the following steps:
6929
6930* Add a page slice inside the `pages` layer
6931* Add the corresponding route inside the `app` layer
6932* Connect the page from the slice with the route
6933
6934To create a page slice, let’s use the [CLI](https://github.com/feature-sliced/cli):
6935
6936```shell
6937fsd pages home
6938```
6939
6940Create a `home-page.vue` file inside the ui segment, access it using the Public API
6941
6942src/pages/home/index.ts
6943
6944```ts
6945export { default as HomePage } from './ui/home-page';
6946```
6947
6948Create a route for this page inside the `app` layer:
6949
6950* src
6951
6952 * app
6953
6954 * routes
6955
6956 * index.vue
6957
6958 * pages
6959
6960 * home
6961
6962 * ui
6963
6964 * home-page.vue
6965
6966 * index.ts
6967
6968Add your page component inside the `index.vue` file:
6969
6970src/app/routes/index.vue
6971
6972```html
6973<script setup>
6974 import { HomePage } from '@/pages/home';
6975</script>
6976
6977
6978<template>
6979 <HomePage/>
6980</template>
6981```
6982
6983## What to do with `layouts`?
6984
6985[Section titled “What to do with layouts?”](#what-to-do-with-layouts)
6986
6987You can place layouts inside the `app` layer, to do this you need to modify the config as follows:
6988
6989nuxt.config.ts
6990
6991```ts
6992export default defineNuxtConfig({
6993 devtools: { enabled: true }, // Not related to FSD, enabled at project startup
6994 alias: {
6995 "@": '../src'
6996 },
6997 dir: {
6998 pages: './src/app/routes',
6999 layouts: './src/app/layouts'
7000 }
7001})
7002```
7003
7004## See also
7005
7006[Section titled “See also”](#see-also)
7007
7008* [Documentation on changing directory config in NuxtJS](https://nuxt.com/docs/api/nuxt-config#dir)
7009* [Documentation on changing router config in NuxtJS](https://nuxt.com/docs/guide/recipes/custom-routing#router-config)
7010* [Documentation on changing aliases in NuxtJS](https://nuxt.com/docs/api/nuxt-config#alias)
7011
7012# Usage with TanStack Query
7013
7014## Where to store keys
7015
7016[Section titled “Where to store keys”](#where-to-store-keys)
7017
7018* In shared
7019
7020 * src/
7021
7022 * app/
7023
7024 * …
7025
7026 * pages/
7027
7028 * …
7029
7030 * widgets/
7031
7032 * …
7033
7034 * features/
7035
7036 * …
7037
7038 * entities/
7039
7040 * …
7041
7042 * shared/
7043
7044 * api/
7045
7046 * queries/ Query factories
7047
7048 * example.ts
7049 * another-example.ts
7050
7051 src/shared/api/index.ts
7052
7053 ```ts
7054 export { exampleQueries } from './queries/example';
7055 ```
7056
7057* In shared split by controllers
7058
7059 If the number of endpoints is large enough and you also want to store them in `shared`, it is better to split everything by controllers and use a public API for each of them.
7060
7061 * src/
7062
7063 * app/
7064
7065 * …
7066
7067 * pages/
7068
7069 * …
7070
7071 * widgets/
7072
7073 * …
7074
7075 * features/
7076
7077 * …
7078
7079 * entities/
7080
7081 * …
7082
7083 * shared/
7084
7085 * api/
7086
7087 * example/
7088
7089 * index.ts
7090 * example.query.ts Query factory with keys and functions for the example controller
7091 * get-example.ts
7092 * create-example.ts
7093 * update-example.ts
7094 * delete-example.ts
7095
7096 * another-example/
7097
7098 * index.ts
7099 * another-example.query.ts Query factory with keys and functions for the another-example controller
7100 * get-another-example.ts
7101 * create-another-example.ts
7102 * update-another-example.ts
7103 * delete-another-example.ts
7104
7105 src/shared/api/example/index.ts
7106
7107 ```ts
7108 export { exampleQueries } from "./example.query";
7109 ```
7110
7111* In entities
7112
7113 *If the project already has a division into entities*, and each request corresponds to a single entity, the cleanest approach is to split by entity. In this case, we suggest using the following structure:
7114
7115 * src/
7116
7117 * app/
7118
7119 * …
7120
7121 * pages/
7122
7123 * …
7124
7125 * widgets/
7126
7127 * …
7128
7129 * features/
7130
7131 * …
7132
7133 * entities/
7134
7135 * example/
7136
7137 * api/
7138
7139 * example.query.ts Query factory with keys and functions
7140 * get-example.ts
7141 * create-example.ts
7142 * update-example.ts
7143 * delete-example.ts
7144
7145 * shared/
7146
7147 * …
7148
7149 If there are connections between entities (for example, the Country entity has a field-list of City entities), you can use the [public API for cross-imports](/docs/reference/public-api#public-api-for-cross-imports).
7150
7151## Where to store mutations
7152
7153[Section titled “Where to store mutations”](#where-to-store-mutations)
7154
7155It is not recommended to mix mutations with queries. There are several options:
7156
7157* In the api segment near the place of use
7158
7159 src/pages/example/api/use-update-example.ts
7160
7161 ```tsx
7162 export const useUpdateExample = () => {
7163 const queryClient = useQueryClient();
7164
7165
7166 return useMutation({
7167 mutationFn: async ({ id, newTitle }) => {
7168 const { data } = await apiClient.patch(`/posts/${ id }`, { title: newTitle });
7169
7170
7171 return data;
7172 },
7173 onSuccess: newPost => {
7174 queryClient.setQueryData(postsQueries.ids(id), newPost);
7175 },
7176 });
7177 };
7178 ```
7179
7180* In shared or entities via mutationFn
7181
7182 src/pages/example/ui/example.tsx
7183
7184 ```tsx
7185 export const Example = () => {
7186 const [title, setTitle] = useState('');
7187
7188
7189 const { mutate, isPending } = useMutation({
7190 mutationFn: mutations.createExample,
7191 });
7192
7193
7194 const handleChange = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
7195 setTitle(value);
7196 };
7197
7198
7199 const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
7200 e.preventDefault();
7201 mutate({ title, userId: DEFAULT_USER_ID });
7202 };
7203
7204
7205 return (
7206 <form onSubmit={ handleSubmit }>
7207 <Input onChange={ handleChange } value={ title } />
7208 <Button type="submit" disabled={ isPending }>Create</Button>
7209 </form>
7210 );
7211 };
7212 ```
7213
7214## Organising queries
7215
7216[Section titled “Organising queries”](#organizing-queries)
7217
7218### Query factory
7219
7220[Section titled “Query factory”](#query-factory)
7221
7222A query factory is an object where key values are functions that return a list of query keys.
7223
7224```ts
7225const keyFactory = {
7226 all: () => ["entity"],
7227 lists: () => [...keyFactory.all(), "list"],
7228};
7229```
7230
7231`queryOptions` — a built-in utility in react-query\@v5
7232
7233One of the best ways to share `queryKey` and `queryFn` in multiple places is to use the `queryOptions` helper function [(more details here)](https://tanstack.com/query/latest/docs/framework/react/guides/query-options).
7234
7235```ts
7236import { queryOptions } from '@tanstack/react-query';
7237
7238
7239const groupOptions = (id: number) => queryOptions({
7240 queryKey: ['groups', id],
7241 queryFn: () => fetchGroups(id),
7242 gcTime: 5 * 1000,
7243});
7244```
7245
7246### 1. Creating a query factory
7247
7248[Section titled “1. Creating a query factory”](#creating-query-factory)
7249
7250src/shared/api/post/post.queries.ts
7251
7252```tsx
7253import { queryOptions } from '@tanstack/react-query';
7254import { getPosts } from './get-posts';
7255import { getDetailPost, type DetailPostQuery } from './get-detail-post';
7256
7257
7258export const POST_QUERIES = {
7259 all: () => ['posts'],
7260 lists: () => [...POST_QUERIES.all(), 'list'],
7261 list: (page: number, limit: number) => queryOptions({
7262 queryKey: [...POST_QUERIES.lists(), page, limit],
7263 queryFn: () => getPosts(page, limit),
7264 placeholderData: prev => prev,
7265 }),
7266 details: () => [...POST_QUERIES.all(), 'detail'],
7267 detail: (query?: DetailPostQuery) => queryOptions({
7268 queryKey: [...POST_QUERIES.details(), query?.id],
7269 queryFn: () => getDetailPost({ id: query?.id }),
7270 }),
7271};
7272```
7273
7274### 2. Using a query factory in application code
7275
7276[Section titled “2. Using a query factory in application code”](#using-query-factory)
7277
7278src/pages/post/ui/post.tsx
7279
7280```tsx
7281import { useParams } from 'react-router';
7282import { postApi } from '@/shared/api/post';
7283import { useQuery } from '@tanstack/react-query';
7284
7285
7286interface Params {
7287 postId: string;
7288}
7289
7290
7291export const Post = () => {
7292 const { postId } = useParams<Params>();
7293
7294
7295 const {
7296 data: post,
7297 error,
7298 isLoading,
7299 isError
7300 } = useQuery(postApi.POST_QUERIES.detail({ id: parseInt(postId ?? '', 10) }));
7301
7302
7303 if (isLoading) {
7304 return (
7305 <div>Loading...</div>
7306 );
7307 }
7308
7309
7310 if (isError || !post) {
7311 return (
7312 <div>{ error?.message }</div>
7313 );
7314 }
7315
7316
7317 return (
7318 <div>
7319 <p>Post id: { post.id }</p>
7320 <div>
7321 <h1>{ post.title }</h1>
7322 <div>
7323 <p>{ post.body }</p>
7324 </div>
7325 </div>
7326 <div>Owner: { post.userId }</div>
7327 </div>
7328 );
7329};
7330```
7331
7332### Benefits of using a query factory
7333
7334[Section titled “Benefits of using a query factory”](#query-factory-benefits)
7335
7336* **Structured requests:** A factory allows you to organise all API requests in one place, making your code more readable and maintainable.
7337* **Convenient access to queries and keys:** The factory provides convenient methods for accessing different types of queries and their keys.
7338* **Easy refetching:** The factory makes it easy to refetch without needing to change query keys in different parts of the application.
7339
7340## Pagination
7341
7342[Section titled “Pagination”](#pagination)
7343
7344Pagination uses the same query factory from the [organising queries](#organizing-queries) section above, with the addition of `placeholderData` to prevent the UI from flickering when navigating between pages.
7345
7346### Usage in a component
7347
7348[Section titled “Usage in a component”](#pagination-usage)
7349
7350src/pages/home/ui/home.tsx
7351
7352```tsx
7353export const Home = () => {
7354 const [page, setPage] = usePageParam(DEFAULT_PAGE);
7355
7356
7357 const { data, isFetching, isLoading } = useQuery(postApi.POST_QUERIES.list(page, DEFAULT_ITEMS_ON_SCREEN));
7358
7359
7360 return (
7361 <>
7362 <Pagination
7363 onChange={ (_, page) => setPage(page) }
7364 page={ page }
7365 count={ data?.totalPages }
7366 variant="outlined"
7367 color="primary"
7368 />
7369 <Posts posts={ data?.posts } />
7370 </>
7371 );
7372};
7373```
7374
7375## Infinite scroll
7376
7377[Section titled “Infinite scroll”](#infinite-scroll)
7378
7379`useInfiniteQuery` is used to implement “load more” or infinite scroll patterns.
7380
7381### 1. Query factory with `infiniteQueryOptions`
7382
7383[Section titled “1. Query factory with infiniteQueryOptions”](#infinite-query-factory)
7384
7385src/shared/api/post/post.queries.ts
7386
7387```tsx
7388import { infiniteQueryOptions } from '@tanstack/react-query';
7389import { getPosts } from './get-posts';
7390
7391
7392export const POST_QUERIES = {
7393 all: () => ['posts'],
7394 lists: () => [...POST_QUERIES.all(), 'list'],
7395 infinite: (limit: number) => infiniteQueryOptions({
7396 queryKey: [...POST_QUERIES.lists(), 'infinite', limit],
7397 queryFn: ({ pageParam }) => getPosts(pageParam, limit),
7398 initialPageParam: 0,
7399 getNextPageParam: (lastPage) =>
7400 lastPage.skip + lastPage.limit < lastPage.total
7401 ? lastPage.skip / lastPage.limit + 1
7402 : undefined,
7403 }),
7404};
7405```
7406
7407### 2. Usage in a component
7408
7409[Section titled “2. Usage in a component”](#infinite-query-usage)
7410
7411src/pages/post-feed/ui/post-feed.tsx
7412
7413```tsx
7414import { useInfiniteQuery } from '@tanstack/react-query';
7415import { postApi } from '@/shared/api/post';
7416
7417
7418export const PostFeed = () => {
7419 const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
7420 useInfiniteQuery(postApi.POST_QUERIES.infinite(10));
7421
7422
7423 const posts = data?.pages.flatMap((page) => page.posts) ?? [];
7424
7425
7426 return (
7427 <>
7428 <Posts posts={ posts } />
7429 { hasNextPage && (
7430 <button onClick={ () => fetchNextPage() } disabled={ isFetchingNextPage }>
7431 { isFetchingNextPage ? 'Loading...' : 'Load more' }
7432 </button>
7433 ) }
7434 </>
7435 );
7436};
7437```
7438
7439## Suspense mode
7440
7441[Section titled “Suspense mode”](#suspense-mode)
7442
7443`useSuspenseQuery` allows you to use React Suspense for handling loading states, removing the need to check `isLoading` manually.
7444
7445### 1. The query factory remains the same
7446
7447[Section titled “1. The query factory remains the same”](#suspense-query-factory)
7448
7449`queryOptions` and `useSuspenseQuery` are compatible — no changes to the factory are needed.
7450
7451### 2. Usage in a component
7452
7453[Section titled “2. Usage in a component”](#suspense-usage)
7454
7455src/pages/post/ui/post.tsx
7456
7457```tsx
7458import { useSuspenseQuery } from '@tanstack/react-query';
7459import { postApi } from '@/shared/api/post';
7460
7461
7462interface PostProps {
7463 id: number;
7464}
7465
7466
7467// isLoading is no longer needed — the component only renders when data is ready
7468export const Post = ({ id }: PostProps) => {
7469 const { data: post } = useSuspenseQuery(postApi.POST_QUERIES.detail({ id }));
7470
7471
7472 return (
7473 <div>
7474 <h1>{ post.title }</h1>
7475 <p>{ post.body }</p>
7476 </div>
7477 );
7478};
7479```
7480
7481### 3. Wrapper in the `app` layer
7482
7483[Section titled “3. Wrapper in the app layer”](#suspense-app-wrapper)
7484
7485src/app/providers/suspense-provider.tsx
7486
7487```tsx
7488import { Suspense, type ReactNode } from 'react';
7489import { ErrorBoundary } from 'react-error-boundary';
7490
7491
7492interface SuspenseProviderProps {
7493 children: ReactNode;
7494}
7495
7496
7497export const SuspenseProvider = ({ children }: SuspenseProviderProps) => (
7498 <ErrorBoundary fallback={ <div>Something went wrong</div> }>
7499 <Suspense fallback={ <div>Loading...</div> }>
7500 { children }
7501 </Suspense>
7502 </ErrorBoundary>
7503);
7504```
7505
7506## `useMutationState`
7507
7508[Section titled “useMutationState”](#use-mutation-state)
7509
7510`useMutationState` allows you to read the state of mutations from any component without passing props — useful for global loading indicators or displaying the status of an operation.
7511
7512### 1. Storing mutation keys
7513
7514[Section titled “1. Storing mutation keys”](#storing-mutation-keys)
7515
7516By analogy with the query factory, mutation keys should be stored in one place alongside the factory:
7517
7518src/shared/api/post/post.queries.ts
7519
7520```ts
7521export const POST_MUTATIONS = {
7522 updateTitle: () => ['post', 'update-title'],
7523 create: () => ['post', 'create'],
7524};
7525```
7526
7527### 2. Naming mutations via `mutationKey`
7528
7529[Section titled “2. Naming mutations via mutationKey”](#naming-mutations)
7530
7531src/features/update-post/api/use-update-post-title.ts
7532
7533```tsx
7534import { POST_MUTATIONS } from '@/shared/api/post';
7535
7536
7537interface UpdatePostTitle {
7538 id: number;
7539 newTitle: string;
7540}
7541
7542
7543export const useUpdatePostTitle = () =>
7544 useMutation({
7545 mutationKey: POST_MUTATIONS.updateTitle(),
7546 mutationFn: ({ id, newTitle }: UpdatePostTitle) =>
7547 apiClient.patch(`/posts/${id}`, { title: newTitle }),
7548 });
7549```
7550
7551### 3. Reading state in another component
7552
7553[Section titled “3. Reading state in another component”](#reading-mutation-state)
7554
7555src/widgets/save-indicator/ui/save-indicator.tsx
7556
7557```tsx
7558import { useMutationState } from '@tanstack/react-query';
7559import { POST_MUTATIONS } from '@/shared/api/post';
7560
7561
7562export const SaveIndicator = () => {
7563 const isPending = useMutationState({
7564 filters: { mutationKey: POST_MUTATIONS.updateTitle(), status: 'pending' },
7565 select: mutation => mutation.state.status,
7566 }).length > 0;
7567
7568
7569 return isPending && (
7570 <span>Saving...</span>
7571 );
7572};
7573```
7574
7575## Organising `QueryProvider`
7576
7577[Section titled “Organising QueryProvider”](#query-provider)
7578
7579src/app/providers/query-provider.tsx
7580
7581```tsx
7582import { type ReactNode } from 'react';
7583import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from '@tanstack/react-query';
7584import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
7585import { toast } from 'sonner';
7586
7587
7588interface QueryProviderProps {
7589 children: ReactNode;
7590 client: QueryClient;
7591}
7592
7593
7594const queryClient = new QueryClient({
7595 queryCache: new QueryCache({
7596 onError: error => {
7597 toast.error(error.message);
7598 },
7599 }),
7600 mutationCache: new MutationCache({
7601 onError: error => {
7602 toast.error(error.message);
7603 },
7604 }),
7605 defaultOptions: {
7606 queries: {
7607 staleTime: 5 * 60 * 1000,
7608 gcTime: 5 * 60 * 1000,
7609 },
7610 },
7611});
7612
7613
7614export const QueryProvider = ({ client, children }: QueryProviderProps) => {
7615 return (
7616 <QueryClientProvider client={ client }>
7617 { children }
7618 <ReactQueryDevtools />
7619 </QueryClientProvider>
7620 );
7621};
7622```
7623
7624## Code generation
7625
7626[Section titled “Code generation”](#code-generation)
7627
7628There are tools for automatic code generation that are less flexible than the manual approach described above. If your Swagger file is well-structured and you are using one of these tools, it may make sense to generate all the code in the `@/shared/api` directory.
7629
7630## Additional advice on API interaction
7631
7632[Section titled “Additional advice on API interaction”](#api-client)
7633
7634By using a custom API client class in the `shared` layer, you can standardise the configuration and work with the API across the project. This allows you to manage logging, headers, and the data exchange format (such as JSON or XML) from one place. This approach simplifies maintenance and development by making changes and updates to API interactions easier.
7635
7636src/shared/api/api-client.ts
7637
7638```tsx
7639import { API_URL } from "@/shared/config";
7640
7641
7642export class ApiClient {
7643 #baseUrl: string;
7644
7645
7646 constructor(url: string) {
7647 this.#baseUrl = url;
7648 }
7649
7650
7651 async handleResponse<TResult>(response: Response): Promise<TResult> {
7652 if (!response.ok) {
7653 throw new Error(`HTTP error! Status: ${ response.status }`);
7654 }
7655
7656
7657 try {
7658 return await response.json();
7659 } catch (error) {
7660 throw new Error("Error parsing JSON response");
7661 }
7662 }
7663
7664
7665 public async get<TResult = unknown>(endpoint: string, queryParams?: Record<string, string | number>): Promise<TResult> {
7666 const url = new URL(endpoint, this.#baseUrl);
7667
7668
7669 if (queryParams) {
7670 Object.entries(queryParams).forEach(([key, value]) => {
7671 url.searchParams.append(key, value.toString());
7672 });
7673 }
7674
7675
7676 const response = await fetch(url.toString(), {
7677 method: 'GET',
7678 headers: {
7679 'Content-Type': 'application/json',
7680 },
7681 });
7682
7683
7684 return this.handleResponse<TResult>(response);
7685 }
7686
7687
7688 public async post<TResult = unknown, TData = Record<string, unknown>>(endpoint: string, body: TData): Promise<TResult> {
7689 const response = await fetch(`${ this.#baseUrl }${ endpoint }`, {
7690 method: 'POST',
7691 headers: {
7692 'Content-Type': 'application/json',
7693 },
7694 body: JSON.stringify(body),
7695 });
7696
7697
7698 return this.handleResponse<TResult>(response);
7699 }
7700}
7701
7702
7703export const apiClient = new ApiClient(API_URL);
7704```
7705
7706## See also
7707
7708[Section titled “See also”](#see-also)
7709
7710* [(GitHub) Sample Project](https://github.com/ruslan4432013/fsd-react-query-example)
7711* [(CodeSandbox) Sample Project](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main)
7712* [About the query factory](https://tkdodo.eu/blog/the-query-options-api)
7713
7714# Usage with SvelteKit
7715
7716It 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:
7717
7718* Initially, SvelteKit offers a file structure inside the `src/routes` folder, while in FSD the routing must be part of the `app` layer.
7719* SvelteKit suggests putting everything not related to routing in the `src/lib` folder.
7720
7721## Let’s set up the config
7722
7723[Section titled “Let’s set up the config”](#lets-set-up-the-config)
7724
7725svelte.config.ts
7726
7727```ts
7728import adapter from '@sveltejs/adapter-auto';
7729import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
7730
7731
7732/** @type {import('@sveltejs/kit').Config}*/
7733const config = {
7734 preprocess: [vitePreprocess()],
7735 kit: {
7736 adapter: adapter(),
7737 files: {
7738 routes: 'src/app/routes', // move routing inside the app layer
7739 lib: 'src',
7740 appTemplate: 'src/app/index.html', // Move the application entry point inside the app layer
7741 assets: 'public'
7742 },
7743 alias: {
7744 '@/*': 'src/*' // Create an alias for the src directory
7745 }
7746 }
7747};
7748export default config;
7749```
7750
7751## Move file routing to `src/app`.
7752
7753[Section titled “Move file routing to src/app.”](#move-file-routing-to-srcapp)
7754
7755Let’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:
7756
7757* src
7758
7759 * app
7760
7761 * index.html
7762 * routes
7763
7764 * pages FSD Pages folder
7765
7766Now, you can create routes for pages within `app` and connect pages from `pages` to them.
7767
7768For example, to add a home page to your project, you need to do the following steps:
7769
7770* Add a page slice inside the `pages` layer
7771* Add the corresponding rooute to the `routes` folder from the `app` layer
7772* Align the page from the slice with the rooute
7773
7774To create a page slice, let’s use the [CLI](https://github.com/feature-sliced/cli):
7775
7776```shell
7777fsd pages home
7778```
7779
7780Create a `home-page.svelte` file inside the ui segment, access it using the Public API
7781
7782src/pages/home/index.ts
7783
7784```ts
7785export { default as HomePage } from './ui/home-page.svelte';
7786```
7787
7788Create a route for this page inside the `app` layer:
7789
7790* src
7791
7792 * app
7793
7794 * routes
7795
7796 * +page.svelte
7797
7798 * index.html
7799
7800 * pages
7801
7802 * home
7803
7804 * ui
7805
7806 * home-page.svelte
7807
7808 * index.ts
7809
7810Add your page component inside the `+page.svelte` file:
7811
7812src/app/routes/+page.svelte
7813
7814```html
7815<script>
7816 import { HomePage } from '@/pages/home';
7817</script>
7818
7819
7820
7821
7822<HomePage/>
7823```
7824
7825## See also.
7826
7827[Section titled “See also.”](#see-also)
7828
7829* [Documentation on changing directory config in SvelteKit](https://kit.svelte.dev/docs/configuration#files)
7830
7831# Docs for LLMs
7832
7833Caution
7834
7835These tools are meant to help AI agents and LLMs apply Feature-Sliced Design concepts more effectively. However, all final architectural decisions must be made by human developers.
7836
7837## llms.txt
7838
7839[Section titled “llms.txt”](#llmstxt)
7840
7841[llms.txt](https://llmstxt.org/) provides LLM-friendly information about a website. It helps large language models understand and use site content more accurately.
7842
7843This site offers the following files:
7844
7845* [llms.txt](/llms.txt) — lists available documentation sets
7846* [llms-small.txt](/llms-small.txt) — a compact version of the Feature-Sliced Design documentation, with non-essential content removed
7847* [llms-full.txt](/llms-full.txt) — the complete Feature-Sliced Design documentation
7848
7849All files are served from the site root (`/`) regardless of the current page path. In deployments with a non-root base URL (e.g. `/documentation/`), links are automatically adjusted with the correct prefix.
7850
7851## Skills
7852
7853[Section titled “Skills”](#skills)
7854
7855[Skills](https://vercel.com/docs/agent-resources/skills) are reusable instruction packages designed for AI coding agents (Codex, Claude Code, OpenCode, and others). Unlike static `llms.txt` files, a Skill can be installed directly into your project and activated when relevant.
7856
7857* [Feature-Sliced Design Skill](https://skills.sh/feature-sliced/skills/feature-sliced-design) — an official, community-driven Skill based on the official FSD documentation.
7858
7859You can install the Skill using `skills` CLI:
7860
7861```bash
7862npx skills add https://github.com/feature-sliced/skills --skill feature-sliced-design
7863```
7864
7865# Layers
7866
7867Layers 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.
7868
7869There are **7 layers** in total, arranged from most responsibility and dependency to least:
7870
7871
7872
78731. App
78742. Processes (deprecated)
78753. Pages
78764. Widgets
78775. Features
78786. Entities
78797. Shared
7880
7881You 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.
7882
7883In practice, layers are folders with lowercase names (for example, `📁 shared`, `📁 pages`, `📁 app`). Adding new layers is *not recommended* because their semantics are standardized.
7884
7885## Import rule on layers
7886
7887[Section titled “Import rule on layers”](#import-rule-on-layers)
7888
7889Layers are made up of *slices* — highly cohesive groups of modules. Dependencies between slices are regulated by **the import rule on layers**:
7890
7891> *A module (file) in a slice can only import other slices when they are located on layers strictly below.*
7892
7893For 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`.
7894
7895Layers 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.
7896
7897In practice, this means that layers App and Shared are made up of segments, and segments can import each other freely.
7898
7899## Layer definitions
7900
7901[Section titled “Layer definitions”](#layer-definitions)
7902
7903This section describes the semantic meaning of each layer to create an intuition for what kind of code belongs there.
7904
7905### Shared
7906
7907[Section titled “Shared”](#shared)
7908
7909This 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.
7910
7911This 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.
7912
7913Here are the segments that you can typically find in this layer:
7914
7915* `📁 api` — the API client and potentially also functions to make requests to specific backend endpoints.
7916* `📁 ui` — the application’s UI kit. Components on this layer should not contain business logic, but it’s okay for them to be business-themed. For example, you can put the company logo and page layout here. Components with UI logic are also allowed (for example, autocomplete or a search bar).
7917* `📁 lib` — a collection of internal libraries. This folder should not be treated as helpers or utilities ([read here why these folders often turn into a dump](https://dev.to/sergeysova/why-utils-helpers-is-a-dump-45fo)). Instead, every library in this folder should have one area of focus, for example, dates, colors, text manipulation, etc. That area of focus should be documented in a README file. The developers in your team should know what can and cannot be added to these libraries.
7918* `📁 config` — environment variables, global feature flags and other global configuration for your app.
7919* `📁 routes` — route constants or patterns for matching routes.
7920* `📁 i18n` — setup code for translations, global translation strings.
7921
7922You 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.
7923
7924### Entities
7925
7926[Section titled “Entities”](#entities)
7927
7928Slices 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.
7929
7930An 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.
7931
7932#### Entity relationships
7933
7934[Section titled “Entity relationships”](#entity-relationships)
7935
7936Entities 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.
7937
7938When 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.
7939
7940For example:
7941
7942entities/artist/model/artist.ts
7943
7944```ts
7945import type { Song } from "entities/song/@x/artist";
7946
7947
7948export interface Artist {
7949 name: string;
7950 songs: Array<Song>;
7951}
7952```
7953
7954entities/song/@x/artist.ts
7955
7956```ts
7957export type { Song } from "../model/song.ts";
7958```
7959
7960Learn more about the `@x` notation in the [Public API for cross-imports](/docs/reference/public-api#public-api-for-cross-imports) section.
7961
7962### Features
7963
7964[Section titled “Features”](#features)
7965
7966This 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.
7967
7968A 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.
7969
7970For 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.
7971
7972Ideally, 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.
7973
7974A 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`).
7975
7976### Widgets
7977
7978[Section titled “Widgets”](#widgets)
7979
7980The 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.
7981
7982If 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.
7983
7984Tip
7985
7986If you’re using a nested routing system (like the router of [Remix](https://remix.run)), 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.
7987
7988In the same way, you can store page layouts on this layer.
7989
7990### Pages
7991
7992[Section titled “Pages”](#pages)
7993
7994Pages 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.
7995
7996There’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.
7997
7998In 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.
7999
8000### Processes
8001
8002[Section titled “Processes”](#processes)
8003
8004Caution
8005
8006This layer has been deprecated. The current version of the spec recommends avoiding it and moving its contents to `features` and `app` instead.
8007
8008Processes are escape hatches for multi-page interactions.
8009
8010This 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.
8011
8012### App
8013
8014[Section titled “App”](#app)
8015
8016All kinds of app-wide matters, both in the technical sense (e.g., context providers) and in the business sense (e.g., analytics).
8017
8018This layer usually doesn’t contain slices, as well as Shared, instead having segments directly.
8019
8020Here are the segments that you can typically find in this layer:
8021
8022* `📁 routes` — the router configuration
8023* `📁 store` — global store configuration
8024* `📁 styles` — global styles
8025* `📁 entrypoint` — the entrypoint to the application code, framework-specific
8026
8027# Public API
8028
8029A 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.
8030
8031In practice, it’s usually implemented as an index file with re-exports:
8032
8033pages/auth/index.js
8034
8035```js
8036export { LoginPage } from "./ui/LoginPage";
8037export { RegisterPage } from "./ui/RegisterPage";
8038```
8039
8040## What makes a good public API?
8041
8042[Section titled “What makes a good public API?”](#what-makes-a-good-public-api)
8043
8044A good public API makes using and integrating into other code a slice convenient and reliable. It can be achieved by setting these three goals:
8045
80461. The rest of the application must be protected from structural changes to the slice, like a refactoring
80472. Significant changes in the behavior of the slice that break the previous expectations should cause changes in the public API
80483. Only the necessary parts of the slice should be exposed
8049
8050The 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:
8051
8052Bad practice, features/comments/index.js
8053
8054```js
8055// ❌ BAD CODE BELOW, DON'T DO THIS
8056export * from "./ui/Comment"; // 👎 don't try this at home
8057export * from "./model/comments"; // 💩 this is bad practice
8058```
8059
8060This 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.
8061
8062## Public API for cross-imports
8063
8064[Section titled “Public API for cross-imports”](#public-api-for-cross-imports)
8065
8066Cross-imports are a situation when one slice imports from another slice on the same layer. Usually that is prohibited by the [import rule on layers](/docs/reference/layers#import-rule-on-layers), 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.
8067
8068For 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.
8069
8070* entities
8071
8072 * A
8073
8074 * @x
8075
8076 * B.ts a special public API just for code inside `entities/B/`
8077
8078 * index.ts the regular public API
8079
8080Then the code inside `entities/B/` can import from `entities/A/@x/B`:
8081
8082```ts
8083import type { EntityA } from "entities/A/@x/B";
8084```
8085
8086The notation `A/@x/B` is meant to be read as “A crossed with B”.
8087
8088Note
8089
8090Try to keep cross-imports to a minimum, and **only use this notation on the Entities layer**, where eliminating cross-imports is often unreasonable.
8091
8092## Environment-specific Public APIs
8093
8094[Section titled “Environment-specific Public APIs”](#environment-specific-public-apis)
8095
8096A slice’s public API should generally be represented by `index.ts`, and freely customizing it is not recommended.
8097
8098However, even modules within the same slice may need to run in different environments. If these modules are exported together from a single `index.ts`, the environment boundary may be broken, or the bundler may incorrectly include code that should not be bundled together. When this problem actually occurs, separate the public API by adding files that match the runtime environment. For an example, see [Server and client public APIs in Usage with Next.js](/docs/guides/tech/with-nextjs#server-and-client-public-apis).
8099
8100## Issues with index files
8101
8102[Section titled “Issues with index files”](#issues-with-index-files)
8103
8104Index 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.
8105
8106### Circular imports
8107
8108[Section titled “Circular imports”](#circular-imports)
8109
8110Circular import is when two or more files import each other in a circle.
8111
8112
8113
8114Pictured above: three files, `fileA.js`, `fileB.js`, and `fileC.js`, importing each other in a circle.
8115
8116These 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.
8117
8118Circular imports can occur without index files, but having an index file presents a clear opportunity 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:
8119
8120pages/home/ui/HomePage.jsx
8121
8122```jsx
8123import { loadUserStatistics } from "../"; // importing from pages/home/index.js
8124
8125
8126export function HomePage() { /* … */ }
8127```
8128
8129pages/home/index.js
8130
8131```js
8132export { HomePage } from "./ui/HomePage";
8133export { loadUserStatistics } from "./api/loadUserStatistics";
8134```
8135
8136This situation creates a circular import, because `index.js` imports `ui/HomePage.jsx`, but `ui/HomePage.jsx` imports `index.js`.
8137
8138To prevent this issue, consider these two principles. If you have two files, and one imports from the other:
8139
8140* When they are in the same slice, always use *relative* imports and write the full import path
8141* When they are in different slices, always use *absolute* imports, for example, with an alias
8142
8143### Large bundles and broken tree-shaking in Shared
8144
8145[Section titled “Large bundles and broken tree-shaking in Shared”](#large-bundles)
8146
8147Some bundlers might have a hard time tree-shaking (removing code that isn’t imported) when you have an index file that re-exports everything.
8148
8149Usually 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`.
8150
8151These 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:
8152
8153* shared/
8154
8155 * ui/
8156
8157 * button/
8158
8159 * …
8160
8161 * text-field/
8162
8163 * …
8164
8165 * carousel/
8166
8167 * …
8168
8169 * accordion/
8170
8171 * …
8172
8173This 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.
8174
8175If 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:
8176
8177* shared/
8178
8179 * ui/
8180
8181 * button/
8182
8183 * index.js
8184
8185 * text-field/
8186
8187 * index.js
8188
8189Then the consumers of these components can import them directly like this:
8190
8191pages/sign-in/ui/SignInPage.jsx
8192
8193```js
8194import { Button } from '@/shared/ui/button';
8195import { TextField } from '@/shared/ui/text-field';
8196```
8197
8198### No real protection against side-stepping the public API
8199
8200[Section titled “No real protection against side-stepping the public API”](#no-real-protection-against-side-stepping-the-public-api)
8201
8202When 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.
8203
8204To catch these issues automatically, we recommend using [Steiger](https://github.com/feature-sliced/steiger), an architectural linter with a ruleset for Feature-Sliced Design.
8205
8206### Worse performance of bundlers on large projects
8207
8208[Section titled “Worse performance of bundlers on large projects”](#worse-performance-of-bundlers-on-large-projects)
8209
8210Having a large amount of index files in a project can slow down the development server, as noted by TkDodo in [his article “Please Stop Using Barrel Files”](https://tkdodo.eu/blog/please-stop-using-barrel-files).
8211
8212There are several things you can do to tackle this issue:
8213
82141. The same advice as in [“Large bundles and broken tree-shaking in Shared” issue](#large-bundles) — have separate index files for each component/library in `shared/ui` and `shared/lib` instead of one big one
82152. 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`.
82163. 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.
8217
8218# Slice groups
8219
8220A slice group places related slices close together within the same layer, making the structure easier to navigate.
8221
8222It is not a required part of FSD. You can introduce slice groups selectively when the number of slices grows large enough that a flat structure becomes hard to browse.
8223
8224## Overview
8225
8226[Section titled “Overview”](#overview)
8227
8228It is a structure for making code easier to read and navigate, and does not affect dependency rules between slices.
8229
8230A slice group is not a slice. It does not have its own segments like `model`, `ui`, or `api`, nor does it have a public API such as `index.ts`. Shared code used across multiple slices should not be placed in a slice group either.
8231
8232
8233
8234Each slice remains independent even when it belongs to a slice group. The isolation and dependency rules between slices do not change.
8235
8236## Why it is needed
8237
8238[Section titled “Why it is needed”](#why-it-is-needed)
8239
8240As the number of slices grows, a flat structure can make it hard to locate related code. When slices that share the same context are scattered across a layer, you end up scanning the entire layer multiple times.
8241
8242A slice group addresses this by bringing related slices closer together, making the structure easier to navigate.
8243
8244## When to consider introducing one
8245
8246[Section titled “When to consider introducing one”](#when-to-consider-introducing-one)
8247
8248Consider introducing a slice group when there is a grouping criterion that feels obvious at a glance, and splitting the structure actually makes it easier to read.
8249
8250* Several slices sharing the same business context are scattered across a layer.
8251* The slice names clearly suggest they belong to the same topic.
8252* The number of slices in a layer has grown to the point where it is hard to take in at a glance.
8253
8254Conversely, it may not be needed yet in the following cases.
8255
8256* Names alone are enough for quick navigation.
8257* There is no natural grouping criterion.
8258* Too few slices would end up in the group.
8259
8260## How to apply it
8261
8262[Section titled “How to apply it”](#how-to-apply-it)
8263
8264### entities
8265
8266[Section titled “entities”](#entities)
8267
8268You can group slices that are close from a domain model perspective.
8269
8270For example, suppose the `entities` layer has accumulated many payment-related slices.
8271
8272**Without a group:**
8273
8274* entities/
8275
8276 * invoice/
8277
8278 * model/
8279
8280 * …
8281
8282 * ui/
8283
8284 * …
8285
8286 * receipt/
8287
8288 * model/
8289
8290 * …
8291
8292 * ui/
8293
8294 * …
8295
8296 * transaction/
8297
8298 * model/
8299
8300 * …
8301
8302 * ui/
8303
8304 * …
8305
8306 * user/
8307
8308 * model/
8309
8310 * …
8311
8312 * ui/
8313
8314 * …
8315
8316 * product/
8317
8318 * model/
8319
8320 * …
8321
8322 * ui/
8323
8324 * …
8325
8326 * …
8327
8328To find the payment-related slices (`invoice`, `receipt`, `transaction`), you would need to scan the entire layer and pick them out one by one.
8329
8330**With a group:**
8331
8332* entities/
8333
8334 * payment/
8335
8336 * invoice/
8337
8338 * model/
8339
8340 * …
8341
8342 * ui/
8343
8344 * …
8345
8346 * receipt/
8347
8348 * model/
8349
8350 * …
8351
8352 * ui/
8353
8354 * …
8355
8356 * transaction/
8357
8358 * model/
8359
8360 * …
8361
8362 * ui/
8363
8364 * …
8365
8366 * user/
8367
8368 * model/
8369
8370 * …
8371
8372 * ui/
8373
8374 * …
8375
8376 * product/
8377
8378 * model/
8379
8380 * …
8381
8382 * ui/
8383
8384 * …
8385
8386 * …
8387
8388Now every payment-related slice can be found directly under `payment/`.
8389
8390Not every slice needs to belong to a group. A slice like `user/`, whose meaning is clear on its own, can remain ungrouped.
8391
8392### pages
8393
8394[Section titled “pages”](#pages)
8395
8396You can group related pages in the pages layer in a similar way. This is one possible example and does not represent the default structure for the pages layer.
8397
8398* pages/
8399
8400 * order/
8401
8402 * create/
8403
8404 * ui/
8405
8406 * …
8407
8408 * detail/
8409
8410 * ui/
8411
8412 * …
8413
8414 * list/
8415
8416 * ui/
8417
8418 * …
8419
8420 * customer/
8421
8422 * detail/
8423
8424 * ui/
8425
8426 * …
8427
8428 * list/
8429
8430 * ui/
8431
8432 * …
8433
8434 * settings/
8435
8436 * ui/
8437
8438 * …
8439
8440 * …
8441
8442This structure helps when there are multiple pages on the same topic, such as list, detail, create, and edit.
8443
8444### Can they be used in features as well?
8445
8446[Section titled “Can they be used in features as well?”](#can-they-be-used-in-features-as-well)
8447
8448Yes, slice groups can be applied to the features layer as well. However, a feature often spans multiple entities rather than being tied to a single domain. This makes it harder to find a natural grouping criterion compared to entities.
8449
8450If you create a group like `features/cart/` without a clear criterion, use cases like `add-to-cart` and `remove-from-cart` will end up there, but cart-related DTOs and mappers may start accumulating as well. At that point the group folder stops serving as a navigational structure and begins acting as a home for the entire cart domain, weakening the principle that features should be split by use case.
8451
8452To introduce a slice group in features, first check whether there are enough slices to warrant a group. If there are only two or three, a flat structure is likely sufficient. You should also check whether the group contains only feature slices, or whether code that belongs in entities has crept in as well.
8453
8454## References
8455
8456[Section titled “References”](#references)
8457
8458* [Russian Telegram FSD community — Discussion on structural grouping, slice groups, and layer extension](https://t.me/c/1216849846/1/2785)
8459* [Russian Telegram FSD community — Nested vs. flat structure, forms and boundaries of slice groups](https://t.me/c/1216849846/1/5051)
8460* [Russian Telegram FSD community — Why there are fewer grouping examples in features, and whether slice groups work in features](https://t.me/c/1216849846/1/80620)
8461* [KakaoPay — A case of applying slice grouping in the pages layer](https://tech.kakaopay.com/post/fsd/#2-slice-grouping-%ED%97%88%EC%9A%A9-%EB%B0%8F-pages-%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B5%AC%EC%84%B1)
8462
8463# Slices and segments
8464
8465## Slices
8466
8467[Section titled “Slices”](#slices)
8468
8469Slices 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.
8470
8471The 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`.
8472
8473The 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.
8474
8475### Zero coupling and high cohesion
8476
8477[Section titled “Zero coupling and high cohesion”](#zero-coupling-high-cohesion)
8478
8479Slices 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*:
8480
8481
8482
8483Image inspired by <https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/>
8484
8485An 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).
8486
8487The independence of slices is enforced by the [import rule on layers](/docs/reference/layers#import-rule-on-layers):
8488
8489> *A module (file) in a slice can only import other slices when they are located on layers strictly below.*
8490
8491### Public API rule on slices
8492
8493[Section titled “Public API rule on slices”](#public-api-rule-on-slices)
8494
8495Inside 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**:
8496
8497> *Every slice (and segment on layers that don’t have slices) must contain a public API definition.*
8498>
8499> *Modules outside of this slice/segment can only reference the public API, not the internal file structure of the slice/segment.*
8500
8501Read more about the rationale of public APIs and the best practices on creating one in the [Public API reference](/docs/reference/public-api).
8502
8503### Slice groups
8504
8505[Section titled “Slice groups”](#slice-groups)
8506
8507Closely 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.
8508
8509
8510
8511## Segments
8512
8513[Section titled “Segments”](#segments)
8514
8515Segments are the third and final level in the organizational hierarchy, and their purpose is to group code by its technical nature.
8516
8517There a few standardized segment names:
8518
8519* `ui` — everything related to UI display: UI components, date formatters, styles, etc.
8520* `api` — backend interactions: request functions, data types, mappers, etc.
8521* `model` — the data model: schemas, interfaces, stores, and business logic.
8522* `lib` — library code that other modules on this slice need.
8523* `config` — configuration files and feature flags.
8524
8525See the [Layers page](/docs/reference/layers#layer-definitions) for examples of what each of these segments might be used for on different layers.
8526
8527You 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.
8528
8529Make 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.
No newline at end of file