skills/nuxt-fsd/SKILL.md
Feature-Sliced Design (FSD) architecture for Nuxt 4+ projects. Use when deciding where to place new code, which layer a module belongs to, how to structure slices and segments, handle cross-slice communication, or when scaffolding new Nuxt pages/features/entities. Covers layer mapping to Nuxt conventions, the thin-page routing pattern, `src/` as FSD root, auto-import strategy, composable patterns, and server-side considerations. Activates on mentions of FSD, feature-sliced, layers, slices, architecture decisions, or when creating new modules in a Nuxt project that follows FSD structure.
npx skillsauth add adamkasper/nuxt-fsd-skills nuxt-fsdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Official FSD docs: https://feature-sliced.design/ LLM reference: https://feature-sliced.design/llms-full.txt
FSD organizes code into layers with a strict dependency rule: each layer can only import from layers below it, never above or sideways. This prevents tangled dependencies — a widget never knows about a page that uses it, a feature never reaches into another feature, and an entity never depends on the UI that displays it.
FSD lives exclusively in src/. The Nuxt app/ directory is the runtime shell — it consumes FSD code from src/ but is not itself organized by FSD. Things like app/composables/, app/middleware/, app/plugins/, and app/layouts/ follow Nuxt conventions, not FSD layers.
src/)Layers are ordered top (most specific) to bottom (most stable). Every layer can only import from layers below it.
pages ← FSD page slices in src/pages/ — compose widgets, features, entities
widgets ← Self-contained UI blocks with coupled logic + presentation
features ← Reusable user interactions — logic is standalone, UI is replaceable
entities ← Business domain models: types, schemas, base queries, formatters
shared ← Framework utilities, UI kit, API client, helpers — zero business logic
| Layer | Contains | Does NOT contain |
|-------|----------|------------------|
| shared | UI kit components, cn(), date/string helpers, API client setup, type utilities | Business logic, domain concepts |
| entities | User, Product, Order types, Zod schemas, base useFetch wrappers, model formatters | User actions, interactive features |
| features | Auth flow, search logic, cart operations, form submission composables | Entity definitions, layout concerns |
| widgets | ProductCard, AppHeader, Sidebar, complete composed sections with own data | Raw entity data access, route logic |
| pages | Page slices assembling widgets/features, page-specific data fetching | Reusable pieces (extract when proven) |
project-root/
├── app/ ← Nuxt 4 app directory (runtime shell, NOT FSD)
│ ├── app.vue ← Root component
│ ├── pages/ ← Thin routing shells (see "Thin page pattern")
│ │ ├── index.vue
│ │ └── products/
│ │ ├── index.vue
│ │ └── [id].vue
│ ├── layouts/ ← Nuxt layouts
│ │ └── default.vue
│ ├── plugins/ ← Nuxt plugins
│ ├── middleware/ ← Nuxt route middleware
│ └── composables/ ← Nuxt global composables
├── src/ ← FSD root — all sliced layers live here
│ ├── pages/ ← FSD page slices (full implementations)
│ │ ├── product-detail/
│ │ │ ├── ui/
│ │ │ │ └── ProductDetailPage.vue
│ │ │ ├── model/
│ │ │ │ └── useProductDetail.ts
│ │ │ └── index.ts
│ │ └── (checkout)/ ← Route group (parentheses)
│ │ ├── _layout/ ← Shared layout for sub-routes
│ │ │ └── CheckoutLayout.vue
│ │ ├── cart/
│ │ │ ├── ui/
│ │ │ └── index.ts
│ │ └── payment/
│ │ ├── ui/
│ │ └── index.ts
│ ├── widgets/ ← FSD widgets layer
│ │ └── product-card/
│ │ ├── ui/
│ │ │ └── ProductCard.vue
│ │ ├── model/
│ │ │ └── useProductCard.ts
│ │ └── index.ts
│ ├── features/ ← FSD features layer
│ │ └── add-to-cart/
│ │ ├── ui/
│ │ │ └── AddToCartButton.vue
│ │ ├── model/
│ │ │ └── useAddToCart.ts
│ │ ├── api/
│ │ │ └── mutations.ts
│ │ └── index.ts
│ ├── entities/ ← FSD entities layer
│ │ └── product/
│ │ ├── ui/
│ │ │ └── ProductPreview.vue
│ │ ├── model/
│ │ │ ├── types.ts
│ │ │ └── schema.ts
│ │ ├── api/
│ │ │ └── queries.ts
│ │ └── index.ts
│ └── shared/ ← FSD shared layer
│ ├── ui/
│ │ ├── UiButton.vue
│ │ └── UiModal.vue
│ ├── lib/
│ │ ├── format-date.ts
│ │ └── cn.ts
│ ├── api/
│ │ └── client.ts
│ └── config/
│ └── constants.ts
├── server/ ← Nuxt server routes (outside FSD client layers)
├── public/ ← Static assets
└── nuxt.config.ts
src/ is the FSD root. All FSD layers (pages/, widgets/, features/, entities/, shared/) live here.app/ is the Nuxt runtime shell. It follows Nuxt conventions, not FSD. It consumes FSD code from src/ via imports.app/pages/ contains thin routing shells only — they delegate to src/pages/ page slices. See "Thin page pattern" below.server/ is outside FSD entirely. Server routes follow Nuxt server conventions.app/pages/*.vue files are routing shells (5–25 lines). They exist solely for Nuxt's file-based routing and delegate all real implementation to FSD page slices in src/pages/.
A thin page:
src/pages/<slice>definePageMeta({ i18n: { ... } }) for i18n path mappings (if using @nuxtjs/i18n with customRoutes: 'meta')definePageMeta() for validation, layout, route key<!-- app/pages/products/[id].vue — thin routing shell -->
<script setup lang="ts">
import { ProductDetailPage } from '~~/src/pages/product-detail'
definePageMeta({
layout: 'default',
validate: async (route) => /^\d+$/.test(route.params.id as string),
i18n: {
paths: {
cs: '/produkty/[id]',
en: '/products/[id]',
},
},
})
</script>
<template>
<ProductDetailPage />
</template>
The actual implementation lives in the FSD page slice:
src/pages/product-detail/
ui/
ProductDetailPage.vue ← Full page implementation
model/
useProductDetail.ts
index.ts ← Exports ProductDetailPage
src/pages/src/pages/ contains full FSD page slices — not Nuxt file-based routes. These are regular FSD slices with ui/, model/, api/, and index.ts.
product-detail/, blog-article-detail/, user-profile/(checkout)/cart/, (checkout)/payment/_layout/ inside a route group holds layout components shared across sub-routessrc/pages/product-detail/
ui/
ProductDetailPage.vue ← Main page component
ProductGallery.vue
ProductSpecs.vue
model/
useProductDetail.ts ← Page-specific composable
api/
queries.ts ← Page-specific data fetching
index.ts ← Public API: exports ProductDetailPage
// src/pages/product-detail/index.ts
export { default as ProductDetailPage } from './ui/ProductDetailPage.vue'
~~/src/ prefixUse Nuxt's ~~/ alias (which resolves to the project root) to import from FSD layers:
// CORRECT — primary import pattern
import { useAuth } from '~~/src/features/auth'
import { type User } from '~~/src/entities/user'
import { UiButton } from '~~/src/shared/ui'
import { ProductDetailPage } from '~~/src/pages/product-detail'
You can optionally configure shorter aliases in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
alias: {
'@shared': fileURLToPath(new URL('./src/shared', import.meta.url)),
'@entities': fileURLToPath(new URL('./src/entities', import.meta.url)),
'@features': fileURLToPath(new URL('./src/features', import.meta.url)),
'@widgets': fileURLToPath(new URL('./src/widgets', import.meta.url)),
},
})
Then use: import { UiButton } from '@shared/ui'. Both patterns are valid — ~~/src/ requires no config, aliases are shorter.
Important: Do NOT rely on Nuxt auto-imports for cross-layer dependencies. Always use explicit imports through the slice's public API (index.ts). This keeps dependencies visible and enforceable.
// CORRECT — explicit import via public API
import { useAuth } from '~~/src/features/auth'
// WRONG — deep import bypassing public API
import { useAuth } from '~~/src/features/auth/model/useAuth'
Nuxt auto-imports are acceptable only for:
ref, computed, useFetch, useRoute, navigateTo, etc.)app/composables/Since src/ lives outside the Nuxt app/ directory, you must explicitly include it in the TypeScript config:
// nuxt.config.ts
export default defineNuxtConfig({
typescript: {
tsConfig: {
include: ['../src/**/*'],
},
sharedTsConfig: {
include: ['../src/**/*'],
},
nodeTsConfig: {
include: ['../src/**/*'],
},
},
})
This ensures all three Nuxt-generated tsconfigs (client, shared, server) include type-checking and auto-completion for FSD code in src/.
A slice is a subfolder inside a layer, named after a business domain concept:
src/features/auth, src/entities/user, src/widgets/product-cardA segment groups code by technical role inside a slice:
src/features/auth/
ui/ ← Vue components
model/ ← Composables, stores (Pinia), state, types
api/ ← Data fetching (useFetch, $fetch, query wrappers)
lib/ ← Helpers specific to this slice
config/ ← Constants, feature flags
index.ts ← Public API (REQUIRED)
user-profile, add-to-cart, product-cardauth not use-auth-hook, product not product-helpersLoginForm.vue, ProductCard.vueuse prefix: useAuth.ts, useProductSearch.tsEvery slice must expose an index.ts. External code imports only from index.ts.
// src/features/auth/index.ts
export { default as LoginForm } from './ui/LoginForm.vue'
export { useAuth } from './model/useAuth'
export { useProvideAuth } from './model/useAuth'
export type { AuthUser, AuthCredentials } from './model/types'
Deep imports are forbidden between slices:
// WRONG
import { useAuth } from '~~/src/features/auth/model/useAuth'
// CORRECT
import { useAuth } from '~~/src/features/auth'
Within-slice relative imports are fine:
// Inside src/features/auth/ui/LoginForm.vue
import { useAuth } from '../model/useAuth'
Start everything in src/pages/. Extract only when reuse is proven.
New functionality needed
│
v
Build inside src/pages/<slice>/
│
v
Used in a 2nd place?
NO --> Keep in src/pages/
YES
│
v
Duplicate it (2 copies is acceptable)
│
v
Used in a 3rd place?
NO --> Keep duplicated
YES
│
v
Extract. Ask: "Is this logic always tied to THIS specific UI?"
YES --> Widget (logic + UI coupled)
NO --> Feature (logic reusable, UI replaceable)
or Entity (pure domain data, no user action)
Rule: Extract when you have evidence (3+ usages), not a prediction. Premature extraction creates unnecessary abstraction.
The critical question: "Can the logic be used WITHOUT this specific UI?"
src/widgets/job-list/
ui/
JobList.vue ← Fetches data, renders list, handles pagination
JobListItem.vue ← Presentational sub-component
model/
useJobList.ts ← Encapsulated composable, not exported alone
index.ts ← Exports only <JobList />
src/features/search/
model/
useSearch.ts ← Works standalone, any UI can consume it
ui/
SearchBar.vue ← Default UI, receives state via props/inject
index.ts ← Exports both useSearch and SearchBar
| Module | Widget or Feature? | Why |
|--------|-------------------|-----|
| Search with debounced results | Feature | useSearch powers different UIs |
| Infinite scroll product list | Widget | Fetch + render always together |
| Auth login form | Feature | useAuth reusable in modal, page, drawer |
| Navigation sidebar | Widget | Nav items + layout always coupled |
| Like/bookmark button | Feature | useLike works standalone |
| Dashboard stats panel | Widget | Chart + data always together |
All state lives in the model/ segment of the relevant slice:
| Pattern | When to use | Placement |
|---------|------------|-----------|
| Simple composable (useState) | Shared state within a feature/entity, SSR-safe | src/features/auth/model/useAuth.ts |
| Provider/Inject (createInjectionState) | State scoped to a component subtree | src/features/cart/model/useCart.ts |
| Pinia store | Global state, devtools, persistence, complex actions | src/entities/user/model/store.ts |
Use createInjectionState from @vueuse/core for scoped state. Always export a throwing variant so consumers get a clear error if the provider is missing:
// src/features/auth/model/useAuth.ts
import { createInjectionState } from '@vueuse/core'
const [useProvideAuth, useAuthRaw] = createInjectionState(() => {
const user = ref<User | null>(null)
return { user }
})
export { useProvideAuth }
export function useAuth() {
const state = useAuthRaw()
if (!state) throw new Error('useAuth must be used within AuthProvider')
return state
}
| What | Where | Example path |
|------|-------|-------------|
| Base queries (read) | Entity api/ segment | src/entities/product/api/queries.ts |
| Mutations (write) | Feature api/ segment | src/features/add-to-cart/api/mutations.ts |
| Page-specific fetching | Page slice api/ or inline | src/pages/product-detail/api/queries.ts |
src/)pages can import from → widgets, features, entities, shared
widgets can import from → features, entities, shared
features can import from → entities, shared
entities can import from → shared
shared can import from → (nothing — only external packages)
app/ (Nuxt shell) can import from any FSD layer in src/.
Slices within the same layer cannot import from each other:
// WRONG — features/auth importing from features/profile
import { useProfile } from '~~/src/features/profile'
// CORRECT — extract shared concept to entities or shared
import { type User } from '~~/src/entities/user'
When entities have legitimate business relationships, use explicit @x cross-references:
// src/entities/order/ui/OrderCard.vue
import { UserAvatar } from '~~/src/entities/user/@x/order'
The @x/<consumer> folder is a controlled cross-import API. Use sparingly.
When slices on the same layer need to coordinate:
entities or sharedmitt) or Pinia store for loose couplingapp/ directory (not FSD)Everything in app/ follows Nuxt conventions, not FSD. These files consume FSD code from src/ but are not themselves organized into FSD layers:
| Nuxt directory | Role | Relation to FSD |
|----------------|------|-----------------|
| app/pages/ | Thin routing shells | Imports page components from src/pages/ |
| app/layouts/ | Nuxt layouts | May import widgets from src/widgets/ |
| app/middleware/ | Route middleware | May import composables from src/features/ or src/entities/ |
| app/plugins/ | Global setup | May import from any src/ layer |
| app/composables/ | Global composables | Only truly app-wide concerns, not FSD-sliced code |
| server/ | Server routes | Entirely outside FSD, follows Nuxt server conventions |
The shared layer has no slices — only segments:
src/shared/
ui/ ← Generic UI components: buttons, modals, inputs, cards
lib/ ← Utility functions: formatDate, cn(), debounce
api/ ← API client setup, interceptors, base fetch wrapper
config/ ← App-wide constants, env helpers, route names
types/ ← Shared TypeScript utility types
Each segment can have its own index.ts for cleaner imports.
src/features/bookmark/
ui/
BookmarkButton.vue
model/
useBookmark.ts
types.ts
api/
mutations.ts
index.ts ← Public API (REQUIRED)
Only create segments you actually need. An entity with only types needs only model/ and index.ts.
app/Wrong: app/features/, app/entities/, app/widgets/, app/shared/.
Right: FSD layers live in src/. Only app/pages/ (thin shells), app/layouts/, app/plugins/, app/middleware/, app/composables/ go in app/.
Wrong: Putting full page implementations in app/pages/products/[id].vue (200+ lines).
Right: app/pages/ files are thin routing shells (5–25 lines) that import from src/pages/.
Wrong: Creating src/features/fancy-button because "it might be reused."
Right: Keep in src/pages/ until 3 actual usages prove the need.
Wrong: Putting useProductSearch (standalone logic) in a widget.
Right: Ask "can this logic work without THIS specific UI?" — yes means Feature.
Wrong: import { x } from '~~/src/features/auth/model/internal'
Right: All exports go through index.ts.
Wrong: src/entities/user importing from src/features/auth.
Right: Dependency flows only downward — entities never reference features.
Wrong: src/features/profile importing from src/features/auth.
Right: Extract shared concept to entities/ or use events.
Wrong: src/shared/lib/useAuth.ts, src/shared/lib/useProductSearch.ts.
Right: Shared is for generic utilities only — zero business logic.
Wrong: Expecting useAuth() to auto-resolve from src/features/auth/model/.
Right: Use explicit imports via ~~/src/features/auth public API.
src/Wrong: Organizing app/composables/, app/middleware/, or server/ into FSD layers.
Right: FSD lives exclusively in src/. Everything else (app/, server/) follows Nuxt conventions.
Before writing or reviewing code, verify:
index.ts public API?index.ts?~~/src/ prefix (or configured aliases)?app/pages/*.vue a thin routing shell (<25 lines)?src/, not in app/ or server/?app/ only contain Nuxt runtime concerns (thin pages, layouts, plugins, middleware)?For existing Nuxt projects adopting FSD with src/ root:
| Code | Approach |
|------|----------|
| New modules | Always follow FSD in src/ |
| Existing app/components/ | Migrate to src/shared/ui/ or appropriate slice ui/ segment |
| Existing app/composables/ | Classify into feature/entity model/ in src/ or keep in app/composables/ if truly global |
| Existing app/utils/ | Move to src/shared/lib/ |
| Existing app/stores/ | Move to entity/feature model/ segments in src/ |
| Existing fat pages | Split into thin shell in app/pages/ + page slice in src/pages/ |
Steps:
src/ directory with FSD layer subdirectoriestypescript includes (tsConfig, sharedTsConfig, nodeTsConfig) to nuxt.config.tssrc/src/pages/ slices~~/src/ and public APIsdevelopment
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.