Structure a frontend app
Examples use React + Next.js (Pages Router) with a neutral listing domain; <feature>/<Entity>
are placeholders. Each rule: portable principle → ▸ Example (React/Next) → ▸ Other stacks.
Backend equivalent: the structure-a-backend-service skill. General style & flow: code-conventions,
git-flow.
Core principle
The router layer stays thin — a route/page just renders a feature component. All app logic lives
under src/, organized by feature. Shared cross-feature building blocks sit in typed top-level
folders; everything single-feature lives inside that feature's module.
1. Repo layout
▸ Example (React/Next):
<app>/
├── pages/ # router (thin): pages → routes; _app, _document, api/ (Next-specific)
├── src/
│ ├── components/ # SHARED UI, type-based (Field/, Layout/, Shell/, ErrorBoundary/, Global*/)
│ ├── modules/ # FEATURE code, feature-based (modules/<feature>/<sub-feature>/)
│ ├── hooks/ # general-purpose hooks (useInterval, useDebounced, SSR-safe helpers)
│ ├── helpers/hooks/ # domain hooks wrapping services/contexts (useMyProfile, useModalState)
│ ├── services/ # one HTTP client (BaseHttp) + one service class per entity
│ ├── queries/ # React Query: queries/<domain>/<domain>.keys.ts + .queries.ts
│ ├── zustand-store/ # client/UI state, one store per feature
│ ├── contexts/ # cross-cutting React Context providers (auth, org, alert, locale)
│ ├── models/ # types: models/request/ + models/response/ (hand-authored DTOs)
│ ├── schemas/ # form validation schemas (*.schema.ts)
│ ├── utils/ constants/ enums/ # domain utils, constants, enums (incl. a Routes enum)
│ └── Config.ts # typed config (public + private) — see write-frontend-code §7
├── styles/ locales/ public/ middleware.ts
▸ Other stacks: same src/ split with a different router folder (App Router app/, React-Router
routes). The point: thin routes, feature modules, typed shared folders.
2. Routing (thin pages → feature modules)
- A page file is a thin wrapper that renders the feature component; no logic in the page.
▸ Example:
pages/listings/index.tsx → <Shell><ListingList/></Shell>; real code in
src/modules/listing/listing-list/.
- RESTful route convention, centralized in an enum:
/<feature> (list), /<feature>/new
(create), /<feature>/[id] (detail), /<feature>/[id]/edit. Reference routes from the enum, never
hard-code path strings.
- App-level providers nest once at the root (
_app.tsx): session → query client → auth guard →
domain contexts → layout. (Next-specific; ▸ Other: a root <Providers> tree.)
3. Feature module pattern
▸ Example — inside src/modules/<feature>/<sub-feature>/:
listing-list/
├── ListingList.tsx # presentational: renders UI using hooks from .actions
├── ListingList.actions.ts # the logic: custom hooks (useListingList, useFetch…, handlers)
├── ListingCard.hook.ts # computed props/callbacks for a sub-component (optional)
├── ListingList.module.scss # component-scoped styles (optional)
└── index.ts # barrel: default export + re-export the .actions hooks
- Split logic from render.
.actions.ts holds the hooks (state, data, side effects, handlers);
the .tsx consumes them and renders. Keeps components readable and logic testable. ▸ Other
stacks: a useX hook module + a dumb component — same container/presentational split, any naming.
- Barrel
index.ts per module so imports are one line:
import ListingList, { useListingList } from 'src/modules/listing/listing-list'.
4. Shared UI vs feature code
src/components/ = shared, type-based (form Field/, Layout/, Shell/, ErrorBoundary/,
Global*). src/modules/<feature>/ = feature-based. Cross-feature → components/contexts/
services/utils; single-feature → its module.
- Design-system primitives come from a shared UI library (placeholder
@org/web-ui), not rebuilt
per app — Button/Modal/Typography/Icon plus shared hooks/utils/theme. ▸ Other stacks: your
component-library package.
5. Naming & imports
- Components/services/models:
PascalCase (ListingCard.tsx; ListingService in
listing.service.ts; Listing.model.ts). Hooks: use* camelCase. Logic files:
*.actions.ts, *.hook.ts. Styles: *.module.scss. Store: *.store.ts. Queries:
*.keys.ts + *.queries.ts. Schemas: *.schema.ts. Folders kebab-case.
- Path aliases, never deep relative (
src/*, @org/*): import x from 'src/modules/...', not ../../../.
6. Types: request vs response
Hand-authored DTOs split by direction: src/models/request/ (params/bodies) and
src/models/response/ (API shapes), plus domain models. No code-gen. ▸ Other stacks: generated
types or a shared schema — keep request and response shapes separate and typed.
Verification
find src -maxdepth 1 -type d shows components, modules, hooks, services, queries, contexts,
models, schemas, utils; the router folder (pages/) is thin (pages just render feature components).
- Each feature lives under
src/modules/<feature>/<sub-feature>/ with .tsx + .actions.ts
(+ .module.scss) + index.ts; logic is in .actions.ts, not the .tsx.
- Imports use
src/* / @org/* aliases (no ../../../); route paths come from the Routes enum.
pnpm type-check + lint clean (lint = Biome here; general principles → code-conventions skill).
Related
write-frontend-code — how to write the code inside these files.
write-frontend-tests — Jest/RTL + Cypress/Cucumber.
structure-a-backend-service — backend equivalent · code-conventions · git-flow.
1---2name: structure-a-frontend-app3description: Use when scaffolding a frontend app, adding a feature or page, or reviewing FE folder layout — thin routing → feature modules, the feature-module pattern (Component + .actions hooks + .hook + styles + barrel), shared-vs-feature code, naming, path aliases, request/response models. React/Next.js reference, framework-flexible.4---56# Structure a frontend app78Examples use React + Next.js (Pages Router) with a neutral `listing` domain; `<feature>`/`<Entity>`9are placeholders. Each rule: portable principle → **▸ Example (React/Next)** → **▸ Other stacks**.10Backend equivalent: the `structure-a-backend-service` skill. General style & flow: `code-conventions`,11`git-flow`.1213## Core principle14The **router layer stays thin** — a route/page just renders a feature component. All app logic lives15under `src/`, organized **by feature**. Shared cross-feature building blocks sit in typed top-level16folders; everything single-feature lives inside that feature's module.1718## 1. Repo layout19▸ **Example (React/Next):**20```21<app>/22├── pages/ # router (thin): pages → routes; _app, _document, api/ (Next-specific)23├── src/24│ ├── components/ # SHARED UI, type-based (Field/, Layout/, Shell/, ErrorBoundary/, Global*/)25│ ├── modules/ # FEATURE code, feature-based (modules/<feature>/<sub-feature>/)26│ ├── hooks/ # general-purpose hooks (useInterval, useDebounced, SSR-safe helpers)27│ ├── helpers/hooks/ # domain hooks wrapping services/contexts (useMyProfile, useModalState)28│ ├── services/ # one HTTP client (BaseHttp) + one service class per entity29│ ├── queries/ # React Query: queries/<domain>/<domain>.keys.ts + .queries.ts30│ ├── zustand-store/ # client/UI state, one store per feature31│ ├── contexts/ # cross-cutting React Context providers (auth, org, alert, locale)32│ ├── models/ # types: models/request/ + models/response/ (hand-authored DTOs)33│ ├── schemas/ # form validation schemas (*.schema.ts)34│ ├── utils/ constants/ enums/ # domain utils, constants, enums (incl. a Routes enum)35│ └── Config.ts # typed config (public + private) — see write-frontend-code §736├── styles/ locales/ public/ middleware.ts37```38▸ *Other stacks:* same `src/` split with a different router folder (App Router `app/`, React-Router39routes). The point: thin routes, feature modules, typed shared folders.4041## 2. Routing (thin pages → feature modules)42- A page file is a **thin wrapper** that renders the feature component; no logic in the page.43 ▸ *Example:* `pages/listings/index.tsx` → `<Shell><ListingList/></Shell>`; real code in44 `src/modules/listing/listing-list/`.45- **RESTful route convention, centralized in an enum:** `/<feature>` (list), `/<feature>/new`46 (create), `/<feature>/[id]` (detail), `/<feature>/[id]/edit`. Reference routes from the enum, never47 hard-code path strings.48- **App-level providers nest once at the root** (`_app.tsx`): session → query client → auth guard →49 domain contexts → layout. (Next-specific; ▸ *Other:* a root `<Providers>` tree.)5051## 3. Feature module pattern52▸ **Example** — inside `src/modules/<feature>/<sub-feature>/`:53```54listing-list/55├── ListingList.tsx # presentational: renders UI using hooks from .actions56├── ListingList.actions.ts # the logic: custom hooks (useListingList, useFetch…, handlers)57├── ListingCard.hook.ts # computed props/callbacks for a sub-component (optional)58├── ListingList.module.scss # component-scoped styles (optional)59└── index.ts # barrel: default export + re-export the .actions hooks60```61- **Split logic from render.** `.actions.ts` holds the hooks (state, data, side effects, handlers);62 the `.tsx` consumes them and renders. Keeps components readable and logic testable. ▸ *Other63 stacks:* a `useX` hook module + a dumb component — same container/presentational split, any naming.64- **Barrel `index.ts` per module** so imports are one line:65 `import ListingList, { useListingList } from 'src/modules/listing/listing-list'`.6667## 4. Shared UI vs feature code68- **`src/components/` = shared, type-based** (form `Field/`, `Layout/`, `Shell/`, `ErrorBoundary/`,69 `Global*`). **`src/modules/<feature>/` = feature-based.** Cross-feature → components/contexts/70 services/utils; single-feature → its module.71- **Design-system primitives come from a shared UI library** (placeholder `@org/web-ui`), not rebuilt72 per app — Button/Modal/Typography/Icon plus shared hooks/utils/theme. ▸ *Other stacks:* your73 component-library package.7475## 5. Naming & imports76- **Components/services/models: `PascalCase`** (`ListingCard.tsx`; `ListingService` in77 `listing.service.ts`; `Listing.model.ts`). **Hooks: `use*` camelCase.** **Logic files:**78 `*.actions.ts`, `*.hook.ts`. **Styles:** `*.module.scss`. **Store:** `*.store.ts`. **Queries:**79 `*.keys.ts` + `*.queries.ts`. **Schemas:** `*.schema.ts`. Folders kebab-case.80- **Path aliases, never deep relative** (`src/*`, `@org/*`): `import x from 'src/modules/...'`, not `../../../`.8182## 6. Types: request vs response83Hand-authored DTOs split by direction: `src/models/request/` (params/bodies) and84`src/models/response/` (API shapes), plus domain models. No code-gen. ▸ *Other stacks:* generated85types or a shared schema — keep request and response shapes separate and typed.8687## Verification88- `find src -maxdepth 1 -type d` shows components, modules, hooks, services, queries, contexts,89 models, schemas, utils; the router folder (`pages/`) is thin (pages just render feature components).90- Each feature lives under `src/modules/<feature>/<sub-feature>/` with `.tsx` + `.actions.ts`91 (+ `.module.scss`) + `index.ts`; logic is in `.actions.ts`, not the `.tsx`.92- Imports use `src/*` / `@org/*` aliases (no `../../../`); route paths come from the Routes enum.93- `pnpm type-check` + lint clean (lint = Biome here; general principles → `code-conventions` skill).9495## Related96- `write-frontend-code` — how to write the code inside these files.97- `write-frontend-tests` — Jest/RTL + Cypress/Cucumber.98- `structure-a-backend-service` — backend equivalent · `code-conventions` · `git-flow`.