Skip to content

08 — Feature Module Internals

Migrated from root technical docs.

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.


{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.


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

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 above

For 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.ts functions — no raw fetch here
  • query invalidation helpers belong here (they are a query concern)

Owns useMutation hooks, mutation factories, and cache invalidation strategy.

  • raw transport functions that perform writes stay in api.ts
  • mutations.ts wraps them in useMutation with 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.

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/ or components/
  • no imports from api.ts (model is lower-level than transport)

Owns TypeScript types and interfaces for the feature’s domain.

  • DTO shapes (may also be co-located in api.ts if small)
  • form value types
  • view-model types

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/)

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 (or api.ts directly if no hook)
  • should not define new query keys or transport logic inline

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.

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

Allowed:

pages/ → components/, queries/, mutations/, model/, api/, types.ts
components/ → model/, types.ts, shared UI
queries/ → api/, query-keys, model/
mutations/ → api/, query-keys
model/ → types.ts (and only types.ts or pure external libs)
api.ts → types.ts, server-fn-client bridge

Forbidden:

  • model/ importing api.ts, queries/, or components/
  • components/ importing pages/
  • api.ts importing React hooks or query state
  • features importing each other’s internals (only index.ts exports)

Apply this structure proportionally.

Tiny feature (e.g. bug-reports):

bug-reports/
api.ts
types.ts
components/
index.ts

Small 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/

Prefer responsibility-based names over generic ones:

Generic (avoid)Preferred
helpers.tsmodel/mappers.ts, model/filters.ts, model/validators.ts
utils.tsSame as above, or inline in the owning file
shared.tsExtract to model/ or components/ with a specific name
common.tsSame as above
user-query.tsqueries.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 validation

Featureapiqueriesmutationsmodelcomponentspagesnotes
adminwrite fns in api.ts; no mutation hooks yet
admin-usersuses adminQueryKeys from admin feature
authclient.ts is auth-client; queries.ts is user+workspace user query
bug-reportsno caching; mutations called directly
dashboardstats/charts; data from other features
email-templateswrite fns in api.ts; invalidation in queries.ts
integrationsdata layer in src/features/workspace-settings/api.ts
site-toolswrite fns in api.ts; page calls them directly
version-syncpolling-based; analytics.ts is model-like
workspace-settingsapp-owned UI + data layer

Package features (packages/feature-*/src/)

Section titled “Package features (packages/feature-*/src/)”
Featureapiqueriesmutationssearch-paramstypesnotes
feature-activityformatting.ts is model-like
feature-people
feature-publications

Feature / FileExceptionReason
admin/api.tsWrite functions co-located with read functionsNo useMutation hooks exist yet; direct calls from page
email-templates/queries.tsinvalidateEmailTemplates in queriesInvalidation is a query concern; no mutation hook needed
admin-users/queries.tsImports query keys from admin/ featureadmin-users is an admin sub-feature; intentional cross-ref
version-sync/analytics.tsModel-like file at feature rootOnly used internally; small enough not to warrant model/
integrations/ (no api/queries)Data delegated to app-owned workspace-settings featureCorrect: 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.tsAuth client bootstrap, not a query fileBetter Auth client init; distinct from queries.ts
auth/links.ts, auth/url.tsNot yet in model/Auth-specific URL/link helpers; low priority to move

  1. Start with pages/ and api.ts. Add queries.ts + query-keys.ts when you need caching. Add mutations.ts when mutation hooks are shared.
  2. Only add model/ when you have pure business logic that would otherwise accumulate inline in page components.
  3. Only add hooks/ when a hook truly belongs to neither queries nor mutations.
  4. Do not create empty placeholder files to fill out the “full shape.”
  5. Export from index.ts only what other features or routes need to consume.