Conventions
June 22, 2026 · View on GitHub
This document defines frontend engineering conventions that affect long-term maintainability: code boundaries, dependency direction, directory structure, naming patterns, and implementation conventions.
When this document conflicts with existing code, tool configuration, generated files, design sources, or other existing facts, the existing facts MUST take precedence. Treat such conflicts as documentation drift or a migration that still needs explicit confirmation. Existing implementations MUST NOT be changed only to match this document unless that is the explicit task.
The directories listed in this document are allowed structures, not required structures. Directories MUST be created only when there is a real need. Directories not listed here MAY be created when they have clear responsibilities and are consistent with the relevant framework, tooling, or common community practice.
When this document does not define a convention, code SHOULD follow the established pattern in nearby code. If no stable local pattern exists, code SHOULD follow the official documentation and common practice of the relevant framework, tool, or library.
Normative Keywords
The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in BCP 14 when, and only when, they appear in uppercase.
MUST: an absolute requirement.MUST NOT: an absolute prohibition.SHOULD: a recommended practice; deviations require a clear reason.SHOULD NOT: a discouraged practice; deviations require a clear reason.MAY: an optional practice.
Principles
- Shared code and feature-specific code MUST be separated.
- Code flow MUST be unidirectional: import dependencies MUST point from application composition toward more shared code, for example app / routes -> features -> shared code.
- Features MUST NOT import directly from other features. Cross-feature composition SHOULD happen in app / routes, or through an appropriate shared module.
- Shared code MUST NOT depend on app, routes, or features.
- Code with a clear owner SHOULD stay close to that owner. Code SHOULD be extracted as shared code only when it is reused across boundaries and has stable semantics.
- Feature-specific code SHOULD live inside the owning feature.
- Colocation MUST NOT break the dependency direction or feature isolation.
- Directories MUST represent real responsibilities, real code, and real maintenance boundaries.
- Projects MUST NOT mechanically create every possible directory for each feature.
Project Structure
Root
The project root MAY contain these directories:
.
├── config/
├── docs/
├── e2e/
├── public/
├── scripts/
└── src/
config/: split project-level tool configuration. Do not create unless needed.docs/: project documentation.e2e/: end-to-end tests. Create only when Playwright or a similar tool is enabled.public/: static assets that are not processed by the bundler.scripts/: project maintenance, code generation, checks, and release scripts. Do not create unless needed.src/: application source code.
Source
src/ MAY contain these directories and entry files:
src/
├── @types/
├── api/
├── app/
├── assets/
├── components/
├── config/
├── features/
├── helpers/
├── hooks/
├── lib/
├── locales/
├── mocks/
├── models/
├── routes/
├── services/
├── stores/
├── styles/
├── testing/
├── utils/
└── index.tsx
@types/: global type declarations and tool-generated declarations.api/: API requests, queries, mutations, and operations shared across features.app/: application composition layer.assets/: application-level bundled assets.components/: shared UI components reused across features.config/: runtime configuration, environment parsing, and application-level configuration objects.features/: primary home for business features.helpers/: project-specific pure helper functions.hooks/: shared React hooks reused across features.lib/: third-party library adapters, configured clients, and infrastructure wrappers.locales/: i18n resource files.mocks/: mock handlers, mock data, and mock utilities.models/: shared frontend data models and boundary transformations.routes/: route-level components.services/: explicit application orchestration services. Do not create unless needed.stores/: shared client state stores. Create only when the corresponding state management approach is introduced.styles/: global style entry points and global CSS.testing/: test utilities, custom render helpers, and test setup helpers.utils/: generic utility functions that can be reused across projects.index.tsx: React bootstrap entry point.
Naming
Folders and Files
- Directory names MUST use
kebab-caseand, unless explicitly exempted, MUST NOT use uppercase letters. - The main name of a file SHOULD use
kebab-case, for exampleuser-form.tsx. - File names MAY use
.to separate the main name from the file responsibility, for exampleusers.types.ts,users.queries.ts,user-list.helpers.ts,user-form.test.tsx, anduser-form.module.css. - React component and hook directory names and file names follow the same rule, and MUST NOT use
PascalCase. - Tool-mandated paths, locale tags, root-level convention files, and generated files MAY follow their own established names.
Singular and Plural Names
- Grouping directories and model / API file subjects for countable business resources SHOULD use plural names, for example
usersandproducts. - Uncountable nouns, capabilities, and domain concepts SHOULD use their natural form, for example
auth,metadata, andtraffic.
Semantic Names
- Code symbols directly related to form values SHOULD keep the
FormValues/formValues/FORM_VALUESsemantic segment, with casing adjusted to the symbol type and the existing naming style. Examples includeUserFormValues,userFormValues, anduserFormValuesSchema. - List items MAY use
<ResourceSingular>ListItemonly when the list item shape is actually different from the detail shape, for exampleUserListItemanduserListItem.
Features
features/*/ contains code private to a specific business feature. Directories inside a feature reuse the responsibilities of the same directory names under src/, but their scope is limited to that feature.
A feature MAY contain these directories:
features/*/
├── api/
├── assets/
├── components/
├── helpers/
├── hooks/
├── models/
├── services/
├── stores/
├── styles/
└── utils/
features/*/api/andfeatures/*/models/MUST serve only the current feature.- Code reused across features SHOULD be promoted to a shared module, or composed at the app / routes layer.
App and Routes
src/index.tsxSHOULD only handle global side-effect imports and React root creation.src/app/SHOULD own app composition, global providers, and global boundaries.src/routes/SHOULD own route-level composition, route params, and route-level loading / error states.- app / routes MAY compose features and shared code.
- app / routes MUST NOT be the home for business resource models, feature-specific components, API request functions, or general-purpose UI components.
Assets
- SVGs used as React components SHOULD use
?reactimports. - SVGs, images, and other assets used as URLs SHOULD use regular imports.
Components
src/components/ contains UI components reused across features and does not depend on feature internals.
components/
├── errors/
├── layouts/
└── ui/
business/MAY be created when there are shared business components with stable semantics that are reused across multiple features.errors/: shared error boundary fallbacks and error display components.layouts/: layout components reused across routes or features.ui/: design system primitives and shared UI components with low business semantics.
Models
models/ contains pure model code for business resources, API data, form data, runtime validation, and data transformations.
Common file suffixes:
<subject>.types.ts: TypeScript types, interfaces, DTOs, and form values.<subject>.schemas.ts: runtime validation schemas.<subject>.constants.ts: constants, options, and mapping tables.<subject>.mappers.ts: pure transformation functions between API DTOs, frontend models, and form values.
Other file suffixes MAY be used when needed:
<subject>.guards.ts: type guards or narrowing functions.<subject>.fixtures.ts: test or development fixtures.<subject>.test.ts: colocated unit tests.
Constants
- Stable primitive constants SHOULD use
UPPER_SNAKE_CASE. - Constants that need to preserve literal types SHOULD use
as const. - When an object shape must be checked while preserving specific inference,
satisfiesSHOULD be used. - Under the current TypeScript configuration, application source MUST NOT use TypeScript
enum. Enum-like runtime values SHOULD useas constobjects / arrays plus union types.
Schemas
- Schema values SHOULD use
camelCaseand end withSchema, for exampleuserSchema.
Mappers
- Mappers MUST be pure functions.
- Mappers SHOULD live in
models/*.mappers.ts.
Dependencies
models/MUST NOT depend onapi/.models/MUST NOT contain network requests, TanStack Query hooks, React components, route composition, or UI side effects.
API
api/ contains API request functions, TanStack Query integration, and operations composed from multiple API requests.
- API code private to a feature SHOULD live in
features/*/api/. - API code SHOULD be promoted to
src/api/only when it is shared by multiple features.
Common file names:
<resource>.requests.ts: backend endpoint request functions. MUST NOT contain React, TanStack Query hooks, or UI side effects.<resource>.operations.ts: frontend async operations composed from multiple request functions. MUST NOT contain React or UI side effects.<resource>.queries.ts: TanStack Query options,queryOptions/infiniteQueryOptions, and query hooks for read operations.<resource>.mutations.ts: TanStack Query mutation hooks, cache invalidation, and cache updates for write operations.
API Naming
GETcollection requests SHOULD uselist<ResourcePlural>; query hooks SHOULD useuse<ResourcePlural>Query; infinite query hooks SHOULD useuse<ResourcePlural>InfiniteQuery.GETdetail requests SHOULD useget<ResourceSingular>; query hooks SHOULD useuse<ResourceSingular>Query.POSTcreate requests SHOULD usecreate<ResourceSingular>; mutation hooks SHOULD useuseCreate<ResourceSingular>Mutation.PATCHpartial update requests SHOULD useupdate<ResourceSingular>; mutation hooks SHOULD useuseUpdate<ResourceSingular>Mutation.PUTreplace requests SHOULD usereplace<ResourceSingular>; mutation hooks SHOULD useuseReplace<ResourceSingular>Mutation.DELETErequests SHOULD usedelete<ResourceSingular>; mutation hooks SHOULD useuseDelete<ResourceSingular>Mutation.- Custom hooks wrapping
useQueriesSHOULD useuse<ResourcePlural>Queries. - Client-side multi-resource operations SHOULD use
<action><ResourcePlural>, for exampledeleteUsers. - Server-side endpoints that apply the same action to multiple resources SHOULD use
bulk<Action><ResourcePlural>, for examplebulkDeleteUsers. - Server-side endpoints that execute multiple operation items in one request SHOULD use
batch<Action><ResourcePlural>, for examplebatchWriteUsers.
URL Search Naming
URL search-related names keep these meanings:
queryParamsMAY mean object-shaped query parameters.queryStringMAY mean a serialized query string without?.searchMAY align with the Web-standardURL.searchmeaning: the full query string including?.searchParamsMAY mean aURLSearchParamsinstance.
TanStack Query
- Reusable query options SHOULD be defined with
queryOptions/infiniteQueryOptions. - Query keys SHOULD include every variable that affects the
queryFnresult. - Query hooks SHOULD reuse query options to avoid redefining the same
queryKeyandqueryFn. - Code that needs a query key SHOULD reuse the
queryKeyfrom the query options instead of redefining the same key.
DTO and Mapping
Dtorepresents an API raw boundary shape. It does not mean every operation must create a dedicated type.- API resource raw shapes SHOULD use
<ResourceSingular>Dto, for exampleUserDto. - Response body raw shapes SHOULD use
<Operation>ResponseDto, for exampleListUsersResponseDto. - Path params SHOULD use
<Operation>PathParams. - Query params, request headers, and request bodies MAY reuse frontend models, form values, or function params directly. Use a
*Dtosuffix only when a separate API raw boundary shape must be represented. - When API raw shapes differ from frontend data structures in fields, naming, nullability, formats, nesting, or semantics, a dedicated
*Dtotype SHOULD be defined and converted through a mapper. - DTO-to-model mapping SHOULD happen inside request functions that return frontend models. Mapper logic SHOULD live in
models/*.mappers.ts. - UI code SHOULD prefer request functions that return frontend models. It should call request functions that return raw DTOs only when raw DTOs are actually needed.
Styling
- Tailwind CSS SHOULD be the baseline styling approach.
- CSS / CSS Modules SHOULD be used only when Tailwind CSS is not suitable.
- Global style entry points SHOULD live in
src/styles/. - Component-scoped CSS Modules SHOULD stay close to the component and use
<component>.module.css.
Testing
- Unit / component tests SHOULD use
*.test.ts/*.test.tsx, and MAY be colocated with the module under test. Unit tests undermodels/SHOULD use*.test.ts. - When Playwright is enabled, e2e tests SHOULD use
*.spec.ts. - Test utilities reused across tests SHOULD live in
src/testing/. - Test helpers used by a single module SHOULD be colocated with that module.
- Production code MUST NOT depend on
src/testing/.
Imports
- Direct imports SHOULD be the baseline import style: import from the actual defining file or from an explicit module entry, not from a directory-level barrel file.
- Feature-level
index.tspublic APIs and global barrel files SHOULD NOT be the baseline. - Small module entries MAY be used, but they must represent real module boundaries.
- Imports MUST follow the dependency direction defined by this document.
- Type-only imports MUST use top-level
import typewhen the imported module provides only types to the current file. - Mixed value/type imports from the same module SHOULD use inline
typespecifiers, for exampleimport { type Value, value } from './value';. - Type-only exports and re-exports MUST use top-level
export type. - Mixed value/type exports and re-exports SHOULD use inline
typespecifiers, for exampleexport { type Value, value } from './value';. - Named import specifiers SHOULD be sorted automatically by Oxlint; import declaration ordering is handled by Oxfmt.
- Application source SHOULD use named exports. Default exports are allowed for tooling configuration, generated files, and external framework or library conventions.
- Implementation modules SHOULD export at declaration sites. Explicit export lists SHOULD be reserved for module entry points or intentional public API selection.