Authentication
This content is not available in your language yet.
Broadly, authentication consists of the following steps:
- Get the credentials from the user
- Send them to the backend
- Store the token to make authenticated requests
How to get credentials from the user
Section titled “How to get credentials from the user”We are assuming that your app is responsible for getting credentials. If you have authentication via OAuth, you can simply create a login page with a link to the OAuth provider’s login page and skip to step 3.
Dedicated page for login
Section titled “Dedicated page for login”Usually, websites have dedicated pages for login, where you enter your username and password. These pages are quite simple, so they don’t require decomposition. Login and registration forms are quite similar in appearance, so they can even be grouped into one page. Create a slice for your login/registration page on the Pages layer:
Directorypages/
Directorylogin/
Directoryui/
- LoginPage.tsx
- RegisterPage.tsx
- index.ts
- …
Here we created two components and exported them both in the index file of the slice. These components will contain forms that are responsible for presenting the user with understandable controls to get their credentials.
Dialog for login
Section titled “Dialog for login”If you need a login dialog that can be reused across multiple pages, you can implement it as a feature responsible for the login user action and flow.
A login dialog typically includes logic such as form state management, input validation, authentication requests and error handling. These responsibilities belong in the features layer because they handle user actions and flows.
Directoryfeatures/
Directorylogin/
Directoryui/
- LoginDialog.tsx
Directorymodel/
- …
Directoryapi/
- …
- index.ts
When multiple pages need the login dialog, they can import and use the feature from each page or from the route configuration in app.
A component responsible only for the common dialog UI and basic interactions can be placed in shared/ui. This component should not include login-specific logic such as authentication requests, input validation or authentication state management.
The UI and logic required for login should be managed in features/login. When necessary, LoginDialog can be implemented by composing the dialog component from shared/ui.
Directoryshared/
Directoryui/
Directorymodal/
- Modal.tsx
- index.ts
Directoryfeatures/
Directorylogin/
Directoryui/
- LoginDialog.tsx
Directorymodel/
- …
Directoryapi/
- …
- index.ts
The following sections use a dedicated login page as the primary example. However, the principles for structuring the login flow also apply to a login dialog.
Client-side validation
Section titled “Client-side validation”Sometimes, especially for registration, it makes sense to perform client-side validation to let the user know quickly that they made a mistake. Validation can take place in the model segment of the login page. Use a schema validation library, for example, Zod for JS/TS, and expose that schema to the ui segment:
import { z } from "zod";
export const registrationData = z.object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"],});Then, in the ui segment, you can use this schema to validate the user input:
import { registrationData } from "../model/registration-schema";
function validate(formData: FormData) { const data = Object.fromEntries(formData.entries()); try { registrationData.parse(data); } catch (error) { // TODO: Show error message to the user }}
export function RegisterPage() { return ( <form onSubmit={(e) => validate(new FormData(e.target))}> <label htmlFor="email">E-mail</label> <input id="email" name="email" required />
<label htmlFor="password">Password (min. 6 characters)</label> <input id="password" name="password" type="password" required />
<label htmlFor="confirmPassword">Confirm password</label> <input id="confirmPassword" name="confirmPassword" type="password" required /> </form> )}How to send credentials to the backend
Section titled “How to send credentials to the backend”Create a function that makes a request to your backend’s login endpoint. This function can either be called directly in the component code using a mutation library (e.g. TanStack Query), or it can be called as a side effect in a state manager. As explained in the guide for API requests, you can put your request either in shared/api or in the api segment of your login page.
Two-factor authentication
Section titled “Two-factor authentication”If your app supports two-factor authentication (2FA), you might have to redirect to another page where a user can enter a one-time password. Usually your POST /login request would return the user object with a flag indicating that the user has 2FA enabled. If that flag is set, redirect the user to the 2FA page.
Since this page is very related to logging in, you can also keep it in the same slice, login on the Pages layer.
You would also need another request function, similar to login() that we created above. Place them together, either in Shared, or in the api segment of the login page.
How to store the token for authenticated requests
Section titled “How to store the token for authenticated requests”Regardless of the authentication scheme you have, be it a simple login & password, OAuth, or two-factor authentication, at the end you will receive a token. This token should be stored so that subsequent requests can identify themselves.
The ideal token storage for a web app is a cookie — it requires no manual token storage or handling. As such, cookie storage needs almost no consideration from the frontend architecture side. If your frontend framework has a server side (for example, Remix), then you should store the server-side cookie infrastructure in shared/api. There is an example in the Authentication section of the tutorial of how to do that with Remix.
Sometimes, however, cookie storage is not an option. In this case, you will have to store the token manually. Apart from storing the token, you may also need to set up logic for refreshing your token when it expires. With FSD, there are several places where you can store the token, as well as several ways to make it available for the rest of the app.
In Shared
Section titled “In Shared”This approach plays well with an API client defined in shared/api because the token is freely available for other request functions that require authentication to succeed. You can make the API client hold state, either with a reactive store or simply a module-level variable, and update that state in your login()/logout() functions.
Automatic token refresh can be implemented as a middleware in the API client — something that can execute every time you make any request. It can work like this:
- Authenticate and store the access token as well as the refresh token
- Make any request that requires authentication
- If the request fails with a status code that indicates token expiration, and there is a token in the store, make a refresh request, store the new tokens, and retry the original request
One of the drawbacks of this approach is that the logic of managing and refreshing the token doesn’t have a dedicated place. This can be fine for some apps or teams, but if the token management logic is more complex, it may be preferable to separate responsibilities of making requests and managing tokens. You can do that by keeping your requests and API client in shared/api, but the token store and management logic in shared/auth.
Another drawback of this approach is that if your backend returns an object of your current user’s information along with the token, you have to store that somewhere or discard that information and request it again from an endpoint like /me or /users/current.
In Entities
Section titled “In Entities”It’s common for FSD projects to have an entity for a user and/or an entity for the current user. It can even be the same entity for both.
To store the token in the User entity, create a reactive store in the model segment. That store can contain both the token and the user object.
Since the API client is usually defined in shared/api or spreaded across the entities, the main challenge to this approach is making the token available to other requests that need it without breaking the import rule on layers:
A module (file) in a slice can only import other slices when they are located on layers strictly below.
There are several solutions to this challenge:
- Pass the token manually every time you make a request
This is the simplest solution, but it quickly becomes cumbersome, and if you don’t have type safety, it’s easy to forget. It’s also not compatible with middlewares pattern for the API client in Shared. - Expose the token to the entire app with a context or a global store like
localStorage
The key to retrieve the token will be kept inshared/apiso 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 orlocalStorageare not set up correctly. - Inject the token into the API client every time it changes
If your store is reactive, you can create a subscription that will update the API client’s token store every time the store in the entity changes. This is similar to the previous solution in that they both create an implicit dependency on higher layers, but this one is more imperative (“push”), while the previous one is more declarative (“pull”).
Once you overcome the challenge of exposing the token that is stored in the entity’s model, you can encode more business logic related to token management. For example, the model segment can contain logic to invalidate the token after a certain period of time, or to refresh the token when it expires. To actually make requests to the backend, use the api segment of the User entity or shared/api.
In Pages/Widgets (not recommended)
Section titled “In Pages/Widgets (not recommended)”It is not recommended to place the token store in pages or in a specific features slice.
Tokens are not state that belongs only to a specific page or a single user action. They are application-wide state used by multiple authenticated API requests and user flows.
For example, if the token store is placed in features/login, another feature cannot directly import it. Different feature slices on the same layer should remain independent from one another.
Similarly, if the token store is placed in pages, modules on lower layers cannot access it. This makes it difficult to reuse the token store in authenticated API requests or other user flows.
Place the token store in shared or in an entities slice representing the current user or session, according to the criteria described above.
Logout and token invalidation
Section titled “Logout and token invalidation”Most applications do not provide a separate page exclusively for logout. Instead, logout functionality is made available wherever it is needed, such as in a header, settings screen, or user menu.
Logout generally consists of the following steps.
- Send an authenticated logout request to the backend.
For example,
POST /logout. - Reset the token store. Remove both the access token and refresh token.
- Reset the current user information and authentication state when necessary.
- Navigate to the login page or another screen when necessary.
The location of the logout request should be determined by the project’s API organization and the scope in which the request is reused.
If all API endpoints are managed in shared/api, authentication-related requests such as login, logout, and token refresh can be placed together.
Directoryshared/
Directoryapi/
- client.ts
Directoryendpoints/
- login.ts
- logout.ts
- refresh-token.ts
- index.ts
If the logout request is used only as part of a specific logout flow, it can be placed in the api segment of features/logout.
If logout is reused across multiple screens and represents an independent user flow that includes token cleanup, user state cleanup, error handling, and navigation, the flow can be extracted into features/logout.
Directoryfeatures/
Directorylogout/
Directoryapi/
- logout.ts
Directoryui/
- LogoutButton.tsx
- index.ts
If logout requires its own state or reusable processing logic, a model segment can be added. There is no need to create unused segments in advance for a simple logout feature.
features/logout coordinates tasks such as sending the logout request and resetting the token store as a single user flow. The token store itself and the token management logic should remain in the previously selected location under shared or entities.
On the other hand, if the logout logic is simple and used in only one or two places, it does not necessarily need to be extracted into a separate feature. It can be composed directly in the page or route configuration where it is used.
Slice names should be based on user actions and flows rather than the UI location where they are displayed. Therefore, even when logout is triggered from a header,
features/logoutis more appropriate thanfeatures/headerwhen the behavior is extracted as an independent user flow.
Automatic logout
Section titled “Automatic logout”The token store and current user state should be reset when the client’s authentication state can no longer be maintained, such as in the following cases
- The user requests to log out.
- The refresh token has expired or is invalid, causing the token refresh request to be rejected.
If the authentication state is not reset, the UI may appear as though the user is still logged in while authenticated API requests continue to fail. Even if the logout request fails, the client can still reset the token store and current user state. However, the server-side session or refresh token may not have been invalidated, so the backend authentication policy should also be taken into account.
If tokens are managed in an entity representing the current user or session, the token reset logic can be placed in the slice’s
modelsegment. If tokens are managed on the Shared layer, they can be separated into a module responsible for authentication, such asshared/auth.