497 lines
14 KiB
Markdown
497 lines
14 KiB
Markdown
# Framework Integration
|
|
|
|
How to set up FSD within specific frameworks. Covers directory placement,
|
|
routing integration, and framework-specific path alias configuration.
|
|
|
|
|
|
## General Principle
|
|
|
|
Place FSD layers inside `src/` to avoid naming conflicts with framework
|
|
directories. The FSD `app/` and `pages/` layers are **not** the same as
|
|
framework directories with the same names (e.g., Next.js `app/`).
|
|
|
|
All FSD projects follow the same `@/<layer>/*` path alias convention. The
|
|
exact configuration differs by framework. See each framework section
|
|
below. Astro is the one exception, using a single `@/*` alias instead.
|
|
|
|
|
|
## Next.js
|
|
|
|
FSD is compatible with both the App Router and the Pages Router. The main
|
|
conflict is that Next.js owns the `app/` and `pages/` folder names, and both
|
|
collide with FSD layer names. Resolve the conflict by moving the Next.js
|
|
routing folders to the project root and importing FSD pages from `src/` into
|
|
them.
|
|
|
|
### App Router
|
|
|
|
The Next.js `app/` folder lives at the project root. **Always create an
|
|
empty `pages/` folder at the project root as well, even when you only use
|
|
the App Router.** Without it, Next.js tries to use `src/pages/` as the Pages
|
|
Router and the build breaks. Add a `pages/README.md` explaining why the
|
|
folder exists.
|
|
|
|
#### Directory structure
|
|
|
|
```text
|
|
my-nextjs-project/
|
|
app/ ← Next.js App Router (routing only)
|
|
layout.tsx
|
|
page.tsx
|
|
profile/
|
|
page.tsx
|
|
api/
|
|
get-example/
|
|
route.ts
|
|
pages/ ← Empty, required even for App Router
|
|
README.md
|
|
src/
|
|
app/ ← FSD app layer
|
|
providers/
|
|
index.tsx ← All providers (QueryClient, theme, etc.)
|
|
styles/
|
|
globals.css
|
|
api-routes/ ← Route Handler implementations (see below)
|
|
index.ts
|
|
get-example-data.ts
|
|
pages/ ← FSD pages layer
|
|
home/
|
|
ui/HomePage.tsx
|
|
index.ts
|
|
profile/
|
|
ui/ProfilePage.tsx
|
|
model/profile.ts
|
|
api/fetch-profile.ts
|
|
index.ts
|
|
widgets/ ← FSD widgets layer (when needed)
|
|
features/ ← FSD features layer (when needed)
|
|
entities/ ← FSD entities layer (when needed)
|
|
shared/ ← FSD shared layer
|
|
ui/
|
|
lib/
|
|
api/
|
|
db/ ← Database queries (see below)
|
|
```
|
|
|
|
#### Wiring Next.js routes to FSD pages
|
|
|
|
```typescript
|
|
// app/layout.tsx
|
|
import { Providers } from '@/app/providers';
|
|
import '@/app/styles/globals.css';
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html lang="en">
|
|
<body><Providers>{children}</Providers></body>
|
|
</html>
|
|
);
|
|
}
|
|
|
|
// app/example/page.tsx: re-export the FSD page (component + metadata)
|
|
export { ExamplePage as default, metadata } from '@/pages/example';
|
|
```
|
|
|
|
Always re-export both the component and `metadata`. Route files contain no logic.
|
|
|
|
### Pages Router
|
|
|
|
The Pages Router uses `pages/` at the project root. The FSD `src/` tree is
|
|
unchanged. Routes re-export from `src/pages/`:
|
|
|
|
```text
|
|
my-nextjs-project/
|
|
pages/ ← Next.js Pages Router (routing only)
|
|
_app.tsx
|
|
api/example.ts ← API route re-export
|
|
example/index.tsx
|
|
src/
|
|
app/
|
|
custom-app/ ← Custom App component
|
|
api-routes/ ← Route Handler implementations
|
|
pages/
|
|
example/
|
|
ui/example.tsx
|
|
index.ts
|
|
```
|
|
|
|
```typescript
|
|
// pages/example/index.tsx
|
|
export { Example as default } from '@/pages/example';
|
|
|
|
// pages/_app.tsx: re-export the custom App from src/app/custom-app
|
|
export { App as default } from '@/app/custom-app';
|
|
```
|
|
|
|
The custom App component itself lives in `src/app/custom-app/` and exports
|
|
`App` from its public API like any other FSD slice.
|
|
|
|
### Middleware and instrumentation
|
|
|
|
`middleware.js` and `instrumentation.js` must live at the **project root**,
|
|
next to the Next.js `app/` and `pages/` folders. Next.js will not detect
|
|
them inside `src/`.
|
|
|
|
### Route Handlers (API routes)
|
|
|
|
Use a dedicated `api-routes` segment in the FSD `app/` layer
|
|
(`src/app/api-routes/`) to host the actual request handlers. The Next.js
|
|
`app/api/*/route.ts` (App Router) or `pages/api/*.ts` (Pages Router) files
|
|
become thin re-exports.
|
|
|
|
**App Router:**
|
|
|
|
```typescript
|
|
// src/app/api-routes/get-example-data.ts
|
|
import { getExamplesList } from '@/shared/db';
|
|
|
|
export const getExampleData = () => {
|
|
try {
|
|
const examplesList = getExamplesList();
|
|
return Response.json({ examplesList });
|
|
} catch {
|
|
return Response.json(null, {
|
|
status: 500,
|
|
statusText: 'Ouch, something went wrong',
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/app/api-routes/index.ts
|
|
export { getExampleData } from './get-example-data';
|
|
|
|
// app/api/example/route.ts
|
|
export { getExampleData as GET } from '@/app/api-routes';
|
|
```
|
|
|
|
**Pages Router:**
|
|
|
|
```typescript
|
|
// src/app/api-routes/get-example-data.ts
|
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
|
|
const config = { api: { bodyParser: { sizeLimit: '1mb' } }, maxDuration: 5 };
|
|
const handler = (req: NextApiRequest, res: NextApiResponse) =>
|
|
res.status(200).json({ message: 'Hello from FSD' });
|
|
|
|
export const getExampleData = { config, handler } as const;
|
|
|
|
// app/api/example.ts
|
|
import { getExampleData } from '@/app/api-routes';
|
|
export const config = getExampleData.config;
|
|
export default getExampleData.handler;
|
|
```
|
|
|
|
FSD is primarily a frontend methodology. If `api-routes` grows to many
|
|
endpoints, consider moving the backend to a separate package in a monorepo.
|
|
|
|
### Database access
|
|
|
|
Place database queries in a `db` segment in `shared/` (`src/shared/db/`).
|
|
Co-locate caching and revalidation logic with the queries themselves.
|
|
|
|
### Path aliases
|
|
|
|
```json
|
|
// tsconfig.json
|
|
{
|
|
"compilerOptions": {
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"@/app/*": ["src/app/*"],
|
|
"@/pages/*": ["src/pages/*"],
|
|
"@/widgets/*": ["src/widgets/*"],
|
|
"@/features/*": ["src/features/*"],
|
|
"@/entities/*": ["src/entities/*"],
|
|
"@/shared/*": ["src/shared/*"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Next.js reads `tsconfig.json` paths automatically. No `next.config.js`
|
|
alias configuration is needed.
|
|
|
|
### Server Components and Public API splitting
|
|
|
|
FSD layers work inside both Server and Client Components. However, the
|
|
standard single `index.ts` public API can cause problems in RSC environments
|
|
because re-exporting client and server code from the same entry point may
|
|
trigger bundler errors or unintended boundary crossings.
|
|
|
|
Split the public API into multiple entry points per environment:
|
|
|
|
```text
|
|
entities/user/
|
|
model/
|
|
user.ts
|
|
ui/
|
|
UserAvatar.tsx ← 'use client', uses hooks
|
|
UserProfileCard.tsx ← Server Component, no hooks
|
|
api/
|
|
user-queries.server.ts ← Server-only data fetching
|
|
index.ts ← Shared exports (types, pure functions)
|
|
index.client.ts ← Client component exports
|
|
index.server.ts ← Server component + server-only exports
|
|
```
|
|
|
|
```typescript
|
|
// entities/user/index.ts: shared (types, pure logic, no components)
|
|
export type { User } from "./model/user";
|
|
export { formatUserName } from "./model/user";
|
|
|
|
// entities/user/index.client.ts: client components only
|
|
export { UserAvatar } from "./ui/UserAvatar";
|
|
|
|
// entities/user/index.server.ts: server components + server-only code
|
|
export { UserProfileCard } from "./ui/UserProfileCard";
|
|
export { fetchUser } from "./api/user-queries.server";
|
|
```
|
|
|
|
```typescript
|
|
// Consumers import from the appropriate entry point:
|
|
|
|
// In a Server Component (pages/profile/ui/ProfilePage.tsx)
|
|
import { UserProfileCard } from "@/entities/user/index.server";
|
|
import type { User } from "@/entities/user";
|
|
|
|
// In a Client Component (features/comment/ui/CommentAuthor.tsx)
|
|
import { UserAvatar } from "@/entities/user/index.client";
|
|
```
|
|
|
|
**Rules for split public APIs:**
|
|
|
|
1. **`index.ts`**: types, constants, and pure functions that work in both
|
|
environments. Default import path.
|
|
2. **`index.client.ts`**: components using `'use client'`, hooks, or
|
|
browser APIs.
|
|
3. **`index.server.ts`**: Server Components and server-only data fetching.
|
|
4. The `index.[env].ts` pattern generalises beyond RSC. Any meta-framework
|
|
with distinct runtime environments can use it (e.g., `index.edge.ts`).
|
|
Verified for Next.js App Router; Nuxt and Astro compatibility is under
|
|
review.
|
|
5. Steiger support for multiple entry points is available or coming in an
|
|
upcoming release. If Steiger flags these files, check for version
|
|
updates.
|
|
|
|
**When NOT to split:** A slice with no client/server boundary concerns uses
|
|
a single `index.ts`. Split only when a slice actually has both client and
|
|
server exports.
|
|
|
|
|
|
## Nuxt 3
|
|
|
|
### Directory structure
|
|
|
|
```text
|
|
my-nuxt-project/
|
|
pages/ ← Nuxt file-based routing
|
|
index.vue ← Route entry, imports from FSD pages layer
|
|
profile.vue
|
|
src/
|
|
app/ ← FSD app layer
|
|
providers/
|
|
pages/ ← FSD pages layer
|
|
home/
|
|
ui/HomePage.vue
|
|
index.ts
|
|
profile/
|
|
ui/ProfilePage.vue
|
|
model/profile.ts
|
|
index.ts
|
|
shared/ ← FSD shared layer
|
|
ui/
|
|
lib/
|
|
api/
|
|
```
|
|
|
|
### Wiring Nuxt routes to FSD pages
|
|
|
|
```vue
|
|
<!-- pages/index.vue: thin route entry -->
|
|
<template>
|
|
<HomePage />
|
|
</template>
|
|
<script setup>
|
|
import { HomePage } from "@/pages/home";
|
|
</script>
|
|
```
|
|
|
|
### Path aliases
|
|
|
|
In addition to the standard `tsconfig.json` mapping, Nuxt requires explicit
|
|
runtime aliases in `nuxt.config.ts`:
|
|
|
|
```typescript
|
|
// nuxt.config.ts
|
|
import { resolve } from "path";
|
|
|
|
export default defineNuxtConfig({
|
|
alias: {
|
|
"@/app": resolve(__dirname, "src/app"),
|
|
"@/pages": resolve(__dirname, "src/pages"),
|
|
"@/widgets": resolve(__dirname, "src/widgets"),
|
|
"@/features": resolve(__dirname, "src/features"),
|
|
"@/entities": resolve(__dirname, "src/entities"),
|
|
"@/shared": resolve(__dirname, "src/shared"),
|
|
},
|
|
});
|
|
```
|
|
|
|
|
|
## Vite + React
|
|
|
|
### Directory structure
|
|
|
|
```text
|
|
my-vite-project/
|
|
src/
|
|
app/ ← FSD app layer
|
|
providers/
|
|
router.tsx
|
|
styles/
|
|
main.tsx ← Entry point
|
|
pages/
|
|
shared/
|
|
index.html
|
|
vite.config.ts
|
|
tsconfig.json
|
|
```
|
|
|
|
### Path aliases
|
|
|
|
Mirror the standard `tsconfig.json` mapping in `vite.config.ts` so the
|
|
Vite resolver agrees with TypeScript:
|
|
|
|
```typescript
|
|
// vite.config.ts
|
|
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import { resolve } from "path";
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
resolve: {
|
|
alias: {
|
|
"@/app": resolve(__dirname, "src/app"),
|
|
"@/pages": resolve(__dirname, "src/pages"),
|
|
"@/widgets": resolve(__dirname, "src/widgets"),
|
|
"@/features": resolve(__dirname, "src/features"),
|
|
"@/entities": resolve(__dirname, "src/entities"),
|
|
"@/shared": resolve(__dirname, "src/shared"),
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
|
|
## Create React App (CRA)
|
|
|
|
CRA is no longer actively maintained. **Migrate to Vite for new projects.**
|
|
|
|
If you must stay on CRA, path aliases require ejecting or using `craco` to
|
|
override the webpack config:
|
|
|
|
```javascript
|
|
// craco.config.js
|
|
const path = require("path");
|
|
|
|
module.exports = {
|
|
webpack: {
|
|
alias: {
|
|
"@/app": path.resolve(__dirname, "src/app"),
|
|
"@/pages": path.resolve(__dirname, "src/pages"),
|
|
"@/widgets": path.resolve(__dirname, "src/widgets"),
|
|
"@/features": path.resolve(__dirname, "src/features"),
|
|
"@/entities": path.resolve(__dirname, "src/entities"),
|
|
"@/shared": path.resolve(__dirname, "src/shared"),
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
|
|
## Astro
|
|
|
|
Astro uses `src/pages/` for file-based routing, which collides with the FSD
|
|
`pages/` layer. Move the FSD pages layer to `src/_pages/` (with the
|
|
underscore prefix) and reserve `src/pages/` for Astro routes.
|
|
|
|
### Directory structure
|
|
|
|
```text
|
|
my-astro-project/
|
|
src/
|
|
pages/ ← Astro routing (thin entry points)
|
|
404.astro
|
|
index.astro
|
|
_pages/ ← FSD pages layer
|
|
home/
|
|
ui/HomePage.astro
|
|
index.ts
|
|
widgets/
|
|
features/
|
|
entities/
|
|
shared/
|
|
```
|
|
|
|
### Wiring Astro routes to FSD pages
|
|
|
|
The Astro route file imports and renders the FSD page, nothing else:
|
|
|
|
```astro
|
|
// src/pages/index.astro
|
|
import { HomePage } from '@/_pages/home';
|
|
<HomePage />
|
|
```
|
|
|
|
### Path aliases (tsconfig.json)
|
|
|
|
Astro projects use a single `@/*` alias instead of one alias per layer. This
|
|
is the convention the FSD Astro guide recommends:
|
|
|
|
```json
|
|
{
|
|
"extends": "astro/tsconfigs/strict",
|
|
"compilerOptions": {
|
|
"paths": {
|
|
"@/*": ["./src/*"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Imports then reference the layer path directly: `@/_pages/home`,
|
|
`@/shared/ui`, `@/entities/user`.
|
|
|
|
### Working with integrations
|
|
|
|
Some Astro integrations (for example, Starlight) use content collections
|
|
that expect content in fixed folders such as `src/content/docs/`. If the
|
|
integration does not allow the path to be changed, leave it as-is. The
|
|
content folder lives alongside FSD layers without collision:
|
|
|
|
```text
|
|
src/
|
|
_pages/ ← FSD pages layer
|
|
content/ ← Integration content (Starlight, etc.)
|
|
docs/
|
|
getting-started.md
|
|
shared/ ← FSD shared layer
|
|
```
|
|
|
|
Let the integration handle its own routing and rendering, while FSD layers
|
|
manage application-specific code.
|
|
|
|
|
|
## Key Reminders for All Frameworks
|
|
|
|
1. **FSD lives in `src/`**: root-level `app/` and `pages/` belong to the
|
|
framework's routing, not FSD.
|
|
2. **Framework route files are thin wrappers**: they import and render FSD
|
|
page components. Business logic stays in FSD pages.
|
|
3. **Path aliases are required**: configure both the bundler and
|
|
`tsconfig.json`.
|
|
4. **Pages First still applies**: regardless of framework, start with code
|
|
in FSD `pages/` and extract only when needed.
|