modules/<spa>/src/features/<feature>/ and owns its full front-end stack — components, hooks, services, state, styles, translations, and types. Each feature is self-contained: it never imports another feature’s internals. When two features need the same code, that code is promoted to src/shared/ and imported from there. This keeps every domain area independent and easy to reason about, and it lets you grow the app by adding slices rather than tangling existing ones.
See SPA overview for the bigger picture and SPA structure for how features sit inside a module.
Why vertical slices
- One domain per feature. Each folder maps to a single area (settings, billing, profile) and holds everything that area needs — no shared “components” dumping ground spanning concerns.
- No cross-feature coupling. A feature never reaches into another feature’s files; shared code lives in
src/shared/, so dependencies are explicit and one-directional. - Clear layering. Backend calls live only in
hooks/;services/stay pure domain logic;store/holds client state. Each layer has one job. - Move-or-delete simplicity. Because a slice is self-contained, you can rename, relocate, or remove a whole domain area by touching one folder.
How it works
Scaffold a feature withspa:feature:create and you get a route file plus a feature folder. The route file wires TanStack Router to the feature’s layouts; the feature folder is organized by layer, each with a single responsibility.
| Subfolder | Purpose |
|---|---|
assets/ | Images, fonts, and other static files used by the feature. |
components/ | Presentational React components for this domain area. |
hooks/ | React hooks — the only layer that talks to the backend (TanStack Query reads and mutations). |
layouts/ | Page and boundary components (page, skeleton, error, not-found). |
services/ | Pure domain logic — no backend calls, no React; just functions over data. |
store/ | Client state for the feature (UI/local state). |
styles/ | Feature-scoped styles. |
translations/ | Feature-scoped i18n messages (see internationalization). |
types/ | TypeScript types for the feature’s data and props. |
utils/ | Small feature-local helpers. |
hooks/ fetch and mutate against the backend, services/ transform data with no side effects, and store/ holds client state. Nothing outside hooks/ should call the backend, and nothing should reach across into a sibling feature.
What spa:feature:create generates
ooneex spa:feature:create --name=<Name> --module=<spa> scaffolds a complete slice. PascalCase drives component names and kebab-case drives paths, so a feature named Settings produces files under settings/ with Settings* components. It generates:
- A route file at
src/routes/<kebab>.tsx— a TanStack Router file route at path/<kebab>. - Four layouts/boundaries in
src/features/<kebab>/layouts/:<Name>Layout.tsx(the page component),<Name>SkeletonLayout.tsx(pending/loading boundary),<Name>ErrorLayout.tsx(error boundary), and<Name>NotFoundLayout.tsx(not-found boundary). - Two example hooks in
src/features/<kebab>/hooks/:useGet<Name>.ts(a TanStack Query READ hook) anduseUpdate<Name>.ts(a MUTATION hook).
@tanstack/react-query if it is missing.
The generated route wires the layouts as the route’s component and its boundaries — one place that maps loading, error, and not-found states to the feature’s own components:
Outlet so nested routes render in place:
Best practices
- One domain per feature. Keep each slice scoped to a single area; if a folder starts serving two concerns, split it.
- Never import another feature’s internals. Cross-feature imports re-couple the slices you worked to separate — treat each feature as a closed box.
- Promote shared code. When two features need the same component, hook, or type, move it to
src/shared/and import it from there. - Keep routes thin. The route file should only wire layouts and boundaries; put real logic in the feature’s layers, not in the route.
- Backend calls only in hooks. Read and mutate through
hooks/; keepservices/pure andstore/for client state, so the data path stays predictable.
CLI command
Scaffold a feature with the generator. It writes the route file, the four layouts, and the two example hooks into the target spa module, and installs@tanstack/react-query if it is missing.
| Option | Description | Default |
|---|---|---|
--name | Feature name. Normalized to PascalCase; a trailing Feature or Layout is stripped. | Prompted if omitted |
--module | Target spa module the feature is generated into. | Prompted if omitted |
--override | Overwrite existing feature files without prompting. | false |
Use with Claude and Codex
The generator ships a matchingspa:feature:create skill. It runs the scaffold and then guides your AI agent through filling in the slice — implementing the layouts, replacing the example hooks with real queries, and adding components, services, and types under the feature folder. Initialize the skills once for your agent:
- Claude
- Codex
Prompt
spa:feature:create --name=Settings --module=dashboard, then implements the layouts and turns the example hooks into the real reads and mutations the settings page needs.