chore: agentic upgrade
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user