08 — Feature Module Internals
Section titled “08 — Feature Module Internals”Generated: 2026-04-22 Status: Active — canonical as of this date.
This document defines the standard internal structure for feature modules in
this repo. It applies to both app-local features (apps/dashboard/src/features/)
and extracted feature packages (packages/feature-*/src/). The structure is
practical and proportional — small features do not need all subfolders.
Canonical feature shape
Section titled “Canonical feature shape”{feature}/ api.ts or api/ Transport wrappers: dynamic import + call() queries.ts or queries/ TanStack Query option factories and keys query-keys.ts Query key factory (may be inlined in queries.ts) mutations.ts or mutations/ Mutation hooks/factories + invalidation strategy model/ Pure feature logic: mappers, filters, validators, search params types.ts Feature-local TypeScript types components/ Reusable feature UI (not page-level) pages/ Page/screen entries rendered by routes hooks/ Feature-specific React hooks (when needed) index.ts Public exports (what other modules may import)Not every feature needs every subfolder. See the “proportionality” section below.
What belongs in each subfolder
Section titled “What belongs in each subfolder”api.ts / api/
Section titled “api.ts / api/”Owns browser-facing transport wrappers. Nothing else.
- async functions calling server functions via
dynamic import + call() - type definitions for DTO shapes if not already in
types.ts - no React hooks, no query keys, no business logic
queries.ts / queries/
Section titled “queries.ts / queries/”Owns TanStack Query option factories and query keys.
Preferred file layout for larger features:
queries/ keys.ts Query key factory (all keys for this feature) list.ts List query options detail.ts Detail/single resource query options index.ts Re-exports all of the aboveFor smaller features, a single queries.ts with an inline query-keys.ts is
acceptable. Both patterns are present in the codebase and are equivalent.
Rules:
- query keys must be domain-scoped, hierarchical, and serializable
- query factories call
api.tsfunctions — no raw fetch here - query invalidation helpers belong here (they are a query concern)
mutations.ts / mutations/
Section titled “mutations.ts / mutations/”Owns useMutation hooks, mutation factories, and cache invalidation strategy.
- raw transport functions that perform writes stay in
api.ts mutations.tswraps them inuseMutationwith defined invalidation behavior- every mutation must define how the cache is updated (invalidate, setQueryData, or optimistic update with rollback)
For features that call mutation API functions directly from page components
without shared mutation hooks, mutations.ts may be omitted until a hook is
actually needed.
model/
Section titled “model/”Owns pure feature-level logic with no side effects. Should be easy to unit test.
- search param normalization and defaults
- filter helpers
- data mappers and view-model shapers
- display-oriented sorting rules
- form validation helpers specific to this feature
- domain constants used only by this feature
Rules:
- no React hooks in
model/ - no imports from
pages/orcomponents/ - no imports from
api.ts(model is lower-level than transport)
types.ts
Section titled “types.ts”Owns TypeScript types and interfaces for the feature’s domain.
- DTO shapes (may also be co-located in
api.tsif small) - form value types
- view-model types
components/
Section titled “components/”Owns reusable feature-specific UI pieces that are not full pages.
- cards, panels, sections, dialogs specific to this feature
- composable pieces used by multiple pages of the same feature
- not generic shared UI (those belong in
src/shared/ui/) - not page-level compositions (those belong in
pages/)
pages/
Section titled “pages/”Owns page/screen-level feature compositions rendered by route files.
- one component per route that uses this feature
- may compose multiple
components/pieces - may read from
queries/using hooks - may call mutations from
mutations.ts(orapi.tsdirectly if no hook) - should not define new query keys or transport logic inline
hooks/
Section titled “hooks/”Owns feature-specific React hooks that do not fit neatly into queries/ or
mutations/.
- hooks encapsulating complex local state + query interaction
- hooks bridging feature events and query cache
Optional — only add hooks/ when a hook genuinely does not belong in queries/
or mutations/. Avoid dumping all hooks here as a catch-all.
index.ts
Section titled “index.ts”The public export surface of the feature.
- export only what other features or routes may legitimately consume
- do not export internal implementation details
- for package features, this is the sole import point for consumers
Dependency direction within a feature
Section titled “Dependency direction within a feature”Allowed:
pages/ → components/, queries/, mutations/, model/, api/, types.tscomponents/ → model/, types.ts, shared UIqueries/ → api/, query-keys, model/mutations/ → api/, query-keysmodel/ → types.ts (and only types.ts or pure external libs)api.ts → types.ts, server-fn-client bridgeForbidden:
model/importingapi.ts,queries/, orcomponents/components/importingpages/api.tsimporting React hooks or query state- features importing each other’s internals (only
index.tsexports)
Proportionality
Section titled “Proportionality”Apply this structure proportionally.
Tiny feature (e.g. bug-reports):
bug-reports/ api.ts types.ts components/ index.tsSmall feature (e.g. email-templates):
email-templates/ api.ts queries.ts query-keys.ts pages/Medium feature (e.g. admin):
admin/ api.ts queries.ts query-keys.ts pages/Large feature (e.g. workspace-settings):
workspace-settings/ ← app-local: UI composition api.ts queries.ts model/ person-taxonomy-form.ts components/ pages/Naming rules
Section titled “Naming rules”Prefer responsibility-based names over generic ones:
| Generic (avoid) | Preferred |
|---|---|
helpers.ts | model/mappers.ts, model/filters.ts, model/validators.ts |
utils.ts | Same as above, or inline in the owning file |
shared.ts | Extract to model/ or components/ with a specific name |
common.ts | Same as above |
user-query.ts | queries.ts |
For model helpers that grow beyond a single file, split by concern:
model/ search-params.ts Search param normalization and defaults filters.ts Filter builders mappers.ts Data shape transformations form-validators.ts Feature-specific form validationCurrent feature inventory
Section titled “Current feature inventory”App-local features (src/features/)
Section titled “App-local features (src/features/)”| Feature | api | queries | mutations | model | components | pages | notes |
|---|---|---|---|---|---|---|---|
admin | ✅ | ✅ | — | — | — | ✅ | write fns in api.ts; no mutation hooks yet |
admin-users | ✅ | ✅ | — | — | ✅ | ✅ | uses adminQueryKeys from admin feature |
auth | — | ✅ | — | — | ✅ | ✅ | client.ts is auth-client; queries.ts is user+workspace user query |
bug-reports | ✅ | — | — | — | ✅ | — | no caching; mutations called directly |
dashboard | — | — | — | — | ✅ | — | stats/charts; data from other features |
email-templates | ✅ | ✅ | — | — | — | ✅ | write fns in api.ts; invalidation in queries.ts |
integrations | — | — | — | — | ✅ | ✅ | data layer in src/features/workspace-settings/api.ts |
site-tools | ✅ | ✅ | — | — | — | ✅ | write fns in api.ts; page calls them directly |
version-sync | ✅ | — | — | — | ✅ | — | polling-based; analytics.ts is model-like |
workspace-settings | ✅ | ✅ | — | ✅ | ✅ | ✅ | app-owned UI + data layer |
Package features (packages/feature-*/src/)
Section titled “Package features (packages/feature-*/src/)”| Feature | api | queries | mutations | search-params | types | notes |
|---|---|---|---|---|---|---|
feature-activity | ✅ | ✅ | — | — | ✅ | formatting.ts is model-like |
feature-people | ✅ | ✅ | ✅ | — | ✅ | |
feature-publications | ✅ | ✅ | ✅ | ✅ | ✅ |
Documented temporary exceptions
Section titled “Documented temporary exceptions”| Feature / File | Exception | Reason |
|---|---|---|
admin/api.ts | Write functions co-located with read functions | No useMutation hooks exist yet; direct calls from page |
email-templates/queries.ts | invalidateEmailTemplates in queries | Invalidation is a query concern; no mutation hook needed |
admin-users/queries.ts | Imports query keys from admin/ feature | admin-users is an admin sub-feature; intentional cross-ref |
version-sync/analytics.ts | Model-like file at feature root | Only used internally; small enough not to warrant model/ |
integrations/ (no api/queries) | Data delegated to app-owned workspace-settings feature | Correct: integrations UI is app-local; data/query layer is in src/features/workspace-settings |
workspace-settings/ (no api) | Historical note (resolved) | No longer applicable; workspace-settings now owns both composition and data/query files |
auth/client.ts | Auth client bootstrap, not a query file | Better Auth client init; distinct from queries.ts |
auth/links.ts, auth/url.ts | Not yet in model/ | Auth-specific URL/link helpers; low priority to move |
How to add a new feature
Section titled “How to add a new feature”- Start with
pages/andapi.ts. Addqueries.ts+query-keys.tswhen you need caching. Addmutations.tswhen mutation hooks are shared. - Only add
model/when you have pure business logic that would otherwise accumulate inline in page components. - Only add
hooks/when a hook truly belongs to neither queries nor mutations. - Do not create empty placeholder files to fill out the “full shape.”
- Export from
index.tsonly what other features or routes need to consume.