19 KiB
AGENTS.md — Frontend Coding Conventions (React & Next.js)
These rules apply to every task in this repository. Follow them exactly. If a rule conflicts with existing code, follow the rule for new code and do not refactor unrelated files. If a rule is unclear, ask before guessing.
0. How this file is organized
- Sections 1–13 are general and apply to every project (React or Next.js).
- Section 14 is framework-specific. Detect the project type first:
nextinpackage.jsondependencies → Next.js project → also follow 14.1.- Otherwise (Vite, CRA, etc.) → React project → also follow 14.2.
- Never apply the rules of the other framework.
1. Core principles
- Readability beats cleverness. Write the simplest code that works. A teammate must understand it on the first read.
- Easy to extend. Small, single-purpose functions and components; clear layers; no hidden coupling.
- Don't repeat yourself, but don't abstract early. Duplicate once, tolerate it twice, extract on the third occurrence (Rule of Three).
- One source of truth. All HTTP goes through one Axios client; all app-wide state lives in one Redux store; all shared UI lives in
components/shared. - Documented. Every function has a doc comment; important places have inline comments.
- Formatted before it runs. Prettier runs automatically before dev and build.
2. Ask before you start (mandatory)
At the start of every task, before writing any code, ask the user these questions and wait for the answers:
- Language: "Should this be written in TypeScript or JavaScript?"
- Ant Design: "Do you need Ant Design for this, or should I use only Tailwind CSS?"
Rules:
- Never assume the answers, even if the repo already contains
tsconfig.jsonor Ant Design. Always ask. - Skip a question only if the user already answered it explicitly in the current task description.
- If Ant Design is not needed, use Tailwind only and do not import anything from
antd. - State the chosen answers in your final summary.
3. Stack
| Concern | Choice |
|---|---|
| Language | Asked every task (TypeScript or JavaScript). If TypeScript: strict: true |
| UI | React 18 (function components + hooks only) |
| Framework | Next.js App Router or React + Vite (detected, see section 0) |
| Global state | Redux Toolkit |
| HTTP | Axios (no RTK Query, no raw fetch) |
| Forms | React Hook Form |
| Styling | Tailwind CSS by default. Ant Design only if the user confirms |
| Formatting | Prettier |
| Testing | Vitest + React Testing Library (Cypress for E2E) |
Do not add a dependency without stating why and asking first.
4. Naming
Files and folders
- Component files:
PascalCase.tsx/.jsx→UserCard.tsx - Everything else:
camelCase.ts/.js→useAuth.ts,userService.ts,authSlice.ts,formatDate.ts - Folders:
camelCase(userManagement/). A folder holding one component plus companion files isPascalCase(UserCard/). - Tests: next to the file →
UserCard.test.tsx - Type files (TS):
user.types.ts - Exception: Next.js reserved names (
page.tsx,layout.tsx,loading.tsx,route.ts) and route folders follow Next.js rules.
Code identifiers
| Kind | Style | Example |
|---|---|---|
| Variables, functions | camelCase | totalPrice, calculateTotal() |
| Components | PascalCase | UserCard |
| Hooks | use + PascalCase |
useDebounce |
| Types, interfaces | PascalCase, no I prefix |
User, UserCardProps |
| Props type | <Component>Props |
UserCardProps |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Booleans | is / has / can / should |
isLoading, hasError |
| Internal handlers | handle + Event |
handleSubmit |
| Callback props | on + Event |
onSubmit |
| Slice | <feature>Slice |
authSlice |
| Thunks | verb + noun | fetchUsers |
| Selectors | select + Name |
selectCurrentUser |
| Service functions | verb + noun | getUsers, createOrder |
- Prefer union types over
enum:type Status = 'idle' | 'loading' | 'failed'. - Names are descriptive. Abbreviate only
id,url,api,props. - Identifiers and comments are in English.
5. Project structure (layer-based)
The shared layers are identical in both project types and live in src/:
src/
components/
shared/ # reusable components used by 2+ pages/features
layout/ # Header, Sidebar, PageLayout
hooks/ # shared custom hooks
services/ # Axios client + API functions (the ONLY place for HTTP)
apiClient.ts
userService.ts
store/
index.ts # configureStore + RootState/AppDispatch
hooks.ts # useAppDispatch, useAppSelector
slices/
types/
utils/ # pure helpers
constants/
styles/
tests/
Only the routing layer differs (see section 14):
- Next.js:
src/app/(routes only). - React (Vite):
src/pages/,src/routes/,src/main.tsx,src/assets/.
Layer rules
- Route files (
app/orpages/) compose components and wire data; no business logic beyond that. - A component used by one page stays in that page's folder. On its third use (or when a second page needs it) move it to
components/shared. components/sharedmust be generic and props-driven: no imports from pages/routes,services, orstore.- Components never call Axios. They call hooks, dispatch thunks, or use data passed as props.
utilsare pure functions (no React, no side effects).- No circular imports. Use the
@/alias forsrc/; avoid../chains deeper than two levels.
6. Writing simple, readable code
- Max nesting: 2 levels (an
ifinside a loop is the limit). Flatten with early returns, guard clauses, or by extracting a named function. - Max function length: ~30 lines. If longer, split into well-named helpers.
- One function, one job. If you need "and" to describe it, split it.
- Max 3 parameters. Beyond that, pass a single object.
- Rule of Three: repeated logic/markup is extracted on the third occurrence into a util, hook, or
sharedcomponent. Do not pre-abstract. - No nested ternaries. Use
if/early return or a lookup object. - Name intermediate values instead of writing long chained expressions.
- No magic numbers or strings: move them to
constants/. - Prefer plain
map/filter/findover cleverreducechains. - Avoid premature optimization: add
useMemo/useCallbackonly for a measured problem or a memoized child.
// Bad: nested and hard to scan
function getLabel(user) {
if (user) {
if (user.isActive) {
return user.name;
}
}
return 'Unknown';
}
// Good: guard clause, flat
function getLabel(user: User | null): string {
if (!user?.isActive) return 'Unknown';
return user.name;
}
7. Documentation and comments
Every function gets a doc comment
Every function must have a short JSDoc block directly above it describing what it does: components, hooks, helpers, services, thunks, selectors, and handlers.
- First line: one clear sentence about the purpose.
- Add
@paramand@returnswhen the function has parameters or a return value. - One line is enough for trivial functions.
- Not required for tiny inline callbacks (e.g. inside
.map()).
/**
* Returns the display name of a user, or "Unknown" when the user is inactive.
* @param user - The user to read the name from.
* @returns The name to show in the UI.
*/
function getLabel(user: User | null): string {
if (!user?.isActive) return 'Unknown';
return user.name;
}
Inline comments for important places
Always comment:
- Why something is done in a non-obvious way (workarounds, browser/device quirks, offline handling).
- Business rules, calculations, and necessary magic values.
- Complex conditions, regex, and data transformations.
- Side effects and cleanup in
useEffect, and why the dependencies are what they are. - Anything that looks like a bug but is intentional.
- Open work as
// TODO: <what and why>.
Do not write comments that only repeat the code (// increment i). No commented-out code. No leftover console.log.
8. Components
- Function declarations for components; arrow functions for inline callbacks.
- One component per file, under ~150 lines. Split when larger.
- Define a props type right above the component (TS) or a
@typedef(JS). Do not useReact.FC. - Named exports only. Default export only where a framework requires it (Next.js special files,
React.lazypages). - Order: hooks → derived values → handlers → effects → early returns → JSX.
- Always render loading, error, and empty states for async UI.
- Extract repeated logic into custom hooks and repeated UI into
components/shared(Rule of Three). - Semantic HTML and basic accessibility (labels, alt text, focus).
type UserCardProps = {
user: User;
onSelect: (id: string) => void;
};
/**
* Shows a clickable card for one user and reports the selection.
*/
export function UserCard({ user, onSelect }: UserCardProps) {
const handleClick = () => onSelect(user.id);
return (
<button type="button" onClick={handleClick}>
{user.name}
</button>
);
}
9. TypeScript / JavaScript
Follow the answer to the question in section 2.
If TypeScript
- No
any. Useunknownand narrow, or define a type. - Type all parameters and exported return values; let inference handle locals.
- Request/response shapes live in
types/and are shared by services and slices. typefor unions and props;interfaceonly when extending.- No non-null assertion (
!) without a comment explaining why it is safe. - Files use
.ts/.tsx.
If JavaScript
- Files use
.js/.jsx. Same naming and structure rules apply. - Describe data shapes with JSDoc (
@typedef,@param {Type},@returns {Type}), kept intypes/. - Document props with a
@typedefabove the component. const/letonly (nevervar), strict equality (===), optional chaining where needed.
10. API layer (all requests via Axios)
- Every HTTP request goes through Axios. Do not use
fetch,XMLHttpRequest, or other HTTP clients. (WebSocket is not HTTP; see 14.2.) - There is one Axios instance:
services/apiClient.ts. It configuresbaseURL, timeout, auth header, and interceptors for token refresh and error normalization. - Requests are made only inside
services/, one file per domain (userService.ts). Services are plain async functions that return data and know nothing about Redux or React./** * Fetches the list of all users from the API. * @returns The users array. */ export async function getUsers(): Promise<User[]> { const { data } = await apiClient.get<User[]>('/users'); return data; } - Endpoint paths are constants (in the service file or
constants/endpoints.ts), not scattered strings. - Errors are normalized once in the interceptor into a single shape (
{ message, status }). Callers showerror.message; they do not parse Axios errors themselves. - Cancel or ignore stale requests with
AbortControllerwhen a component can unmount mid-request.
11. Global state (Redux Toolkit)
The store holds only these categories. Anything else stays local (useState, or a small hook calling a service).
| Category | Slice | Holds |
|---|---|---|
| Auth & current user | authSlice |
token, profile, permissions |
| General UI settings | uiSlice |
theme, language, sidebar state |
| Device connection | deviceSlice |
online/offline status, connected device info (only if the project talks to devices) |
| Shared cross-page data | one slice per domain (e.g. lookupsSlice) |
data needed by 2+ pages |
| Shopping cart | cartSlice |
cart items and quantities (header badge, cart page, product pages) |
Rules:
- One slice per feature in
store/slices/. Slice shape for fetched data:{ items, status: 'idle' | 'loading' | 'succeeded' | 'failed', error: string | null }. - Async work uses
createAsyncThunkthat calls a service; handlepending/fulfilled/rejectedinextraReducers. - Use typed
useAppDispatch/useAppSelectoronly (plain equivalents in JS). - Write selectors beside the slice; components do not read deep state shapes directly.
- Page-local data (a table on one page, form state, open/closed flags) does not go in Redux.
- Keep the state shape documented: when adding a slice, add one line to the table above.
12. Formatting (Prettier is mandatory)
Code must be formatted with Prettier before the project runs in dev or build.
package.json scripts:
{
"scripts": {
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"predev": "npm run format",
"prebuild": "npm run format",
"typecheck": "tsc --noEmit",
"lint": "eslint . --max-warnings 0"
}
}
(predev and prebuild run automatically before npm run dev / npm run build. typecheck is TypeScript only.)
.prettierrc:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
- Run
npm run formatafter editing files and before reporting a task as done. - Never hand-format against Prettier; never use
// prettier-ignorewithout a reason in a comment. - ESLint passes with zero warnings (
react-hooks,@typescript-eslintif TS, import order). - Import order: React → third-party →
@/aliases → relative → styles. Remove unused imports.
13. Styling, testing, and git
Styling
- Default is Tailwind CSS only. Use Ant Design only if the user said yes in section 2.
- Tailwind utilities for layout/spacing;
clsxfor conditional classes; extract a component when the same class list repeats 3+ times. - When Ant Design is approved: customize via theme tokens, not CSS overrides, and do not mix it with custom components that do the same job.
- No inline
styleexcept for truly dynamic values. No hard-coded colors or sizes: use tokens. - Mobile-first and responsive; for Persian UI use RTL-safe logical utilities (
ms-*,me-*,ps-*,pe-*).
Testing
- Test behavior via React Testing Library; query by role/label/text.
- Mock the network at the service boundary; never call real APIs.
- New utils, slices, and non-trivial components ship with tests; bug fixes ship with a regression test.
Git
- Branches:
feature/<name>,fix/<name>. Commits: Conventional Commits (feat:,fix:,refactor:,test:,docs:,chore:). - Keep changes small and focused; do not mix refactors with features.
14. Framework-specific rules
14.1 Next.js (App Router)
src/
app/ # routes only: page.tsx, layout.tsx, loading.tsx, route.ts
components/ hooks/ services/ store/ types/ utils/ constants/ styles/
- Components are Server Components by default. Add
'use client'to the smallest leaf that needs state, effects, or browser APIs. - Public/SEO data is fetched on the server (the same Axios client may be used server-side), not through Redux.
- Redux and browser-only hooks live in client components, provided via a client
Providerscomponent. Create the store only if client state is actually needed. - Every page defines metadata (
generateMetadata) with title and description. - Use
next/imageandnext/link. Use route handlers (route.ts) only when an API layer is truly needed. - Never touch
window/localStorageduring server rendering. - Default exports are allowed for
page,layout,loading,error,not-found.
14.2 React (Vite SPA / PWA / embedded UI)
src/
assets/ # images, fonts, icons
pages/ # one folder per route; page-specific components live here
routes/ # router config, route guards
main.tsx
components/ hooks/ services/ store/ types/ utils/ constants/ styles/ tests/
- Lazy-load pages with
React.lazy+Suspense. - Assume unreliable or no network if the app targets devices (vending, PWA). Handle offline state explicitly; never block the UI on a failed request.
- Device communication over HTTP goes through Axios in
services/. WebSocket/serial live in a dedicatedservices/module with typed messages and reconnect logic, updatingdeviceSlice. - For ESP32 / Orange Pi targets keep bundles small: avoid heavy dependencies and unused polyfills.
- Document PWA caching/queue strategy in the PR description.
15. Refactoring an existing project
When asked to refactor a project to match this file:
- Detect the project type (section 0) and ask the questions in section 2 before starting. Do not migrate between TypeScript and JavaScript unless explicitly asked.
- Audit first. Scan the project and report what violates these rules (structure, naming, HTTP calls outside
services/, missing doc comments, deep nesting, duplicated code, unformatted files). Do not change code yet; present a short plan and wait for approval. - Preserve behavior. A refactor must not change what the app does. No new features, no UI changes, no dependency upgrades.
- Work in small steps, one concern at a time, in this order:
- Add Prettier config and scripts, then format the project (formatting-only commit).
- Centralize HTTP: create
apiClientand move every request intoservices/. - Restructure folders and rename files per sections 4 and 5; update imports.
- Organize global state per section 11; move page-local state out of Redux.
- Extract duplicated code (Rule of Three) and move reusable components to
components/shared. - Flatten nesting and split long functions (section 6).
- Add doc comments and important inline comments (section 7).
- Verify after every step: run
format,typecheck(TS only),lint, andtest, and make sure the project still builds. Fix breakage before moving on. - One commit per step, using Conventional Commits (
refactor: ...). - Do not touch anything you are unsure about; list it in the final report instead.
16. Agent checklist (before finishing any task)
- Did I detect the project type and ask the TypeScript/JavaScript and Ant Design questions (section 2)?
- Did I read the surrounding code and match its patterns?
- Is new code in the correct layer (section 5)? Any new HTTP call goes in
services/viaapiClient. - Nesting ≤ 2, functions ≤ ~30 lines, no third copy of repeated code?
- Does every function have a JSDoc description, and do important places have inline comments?
- Did I run
npm run format,typecheck(TS only),lint, andtest? - Did I summarize what changed, the chosen language/UI library, assumptions, and follow-ups?
- Did I avoid touching unrelated files, configs, and dependencies?