--- title: Coding Standards tags: [development] --- # Coding Standards This page is the authoritative source for code style, file layout, and review rules across both repos. The UI section is a condensation of `backend/.cursor/rules/ui-development-standards.mdc` — **that file is binding for any UI work**. --- ## 1. TypeScript ### Backend (`tsconfig.json`) - `strict: true` — no implicit any, strict null checks, all the trimmings. - `target: ES2020`, `module: commonjs` (Node 22 ESM is not used yet). - Path aliases (use them, do not write deep relative imports): ```ts import { config } from "@shared/config"; // src/shared/config import { paymentSvc } from "@services/payment"; // src/services/payment import { redis } from "@infrastructure/redis"; // src/infrastructure/redis ``` - `declaration: true` + `sourceMap: true` — keep this on. Source maps are required by Sentry stack traces. ### Frontend - `strict: true`, `jsx: preserve` for Next. - All component props **must** be typed and exported (`export type ComponentNameProps = …`). - Prefer `type` over `interface` except when declaring something that must be extendable from a consumer module. - Re-export from a barrel `index.ts` per folder — never deep-import (`import x from 'src/components/foo/internal/x'`). --- ## 2. ESLint & Prettier ### Backend ESLint (`backend/eslint.config.js`) Flat config, TypeScript-only: - `@typescript-eslint/no-unused-vars: warn` - `@typescript-eslint/no-explicit-any: warn` — `any` is allowed when you justify it, but failing this rule will be flagged in code review. - `no-console: off` — backend uses `src/utils/logger.ts` (a thin `console.log` wrapper) so console statements are fine in scripts. Inside services, prefer `log(...)` from the logger so you can later swap to structured logging. Commands: ```bash npm run lint # check npm run lint:fix # auto-fix npm run format # prettier --write src/**/*.ts npm run typecheck # tsc --noEmit ``` ### Frontend ESLint (`frontend/eslint.config.mjs`) A heavier config combining `typescript-eslint`, `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-import`, `eslint-plugin-perfectionist` (for sorting), and `eslint-plugin-unused-imports`. The most-cited rule in PR review: **import sorting**. The `perfectionist` plugin enforces this order — `eslint --fix` will reorder automatically: ```ts // 1. Style imports import './styles.css'; // 2. Side effects import 'react-hot-toast'; // 3. Type imports (always isolated) import type { ComponentProps } from '@mui/material'; // 4. External libraries import { useState } from 'react'; import { motion } from 'framer-motion'; // 5. MUI components import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; // 6. Internal (routes → hooks → utils → components → sections → auth → types) import { paths } from 'src/routes/paths'; import { useAuthContext } from 'src/auth/hooks'; import { formatNumber } from 'src/utils/format-number'; import { Iconify } from 'src/components/iconify'; import { HomeHero } from 'src/sections/home'; import type { User } from 'src/types/user'; ``` Run before pushing: ```bash yarn lint yarn lint:fix ``` ### Prettier Both repos use Prettier defaults from the local config: - 2-space indent - single quotes - trailing commas (`es5` on frontend, `all` on backend) - semicolons on `yarn lint:fix` / `npm run format` both run Prettier. --- ## 3. Naming conventions | Kind | Convention | Example | |------|------------|---------| | TS files (general) | kebab-case | `format-number.ts` | | React component file | kebab-case folder, `component.tsx` inside | `request-card/component.tsx` | | Class | PascalCase | `class PaymentService` | | Function | camelCase | `createPayInIntent()` | | React component | PascalCase | `RequestCard` | | Hook | camelCase starting with `use` | `useSocket`, `useAuthContext` | | Constant | SCREAMING_SNAKE | `MAX_FILE_SIZE` | | Drizzle table | camelCase (schema) / snake_case (SQL) | `purchaseRequests` / `purchase_requests` | | Route handler | `` | `getRequestById`, `createOffer` | | Express route file | `Routes.ts` | `paymentRoutes.ts` | --- ## 4. Backend — service file layout A typical service folder under `src/services/`: ``` src/services/marketplace/ ├── index.ts # Barrel — only public exports ├── marketplaceRoutes.ts # Router (express.Router) — auth middleware, validation, controller calls ├── marketplaceController.ts # HTTP layer — parses req, calls service, formats response envelope └── marketplaceService.ts # Business logic — calls repository layer, throws domain errors ``` ### Response envelope Every JSON response (success or error) uses the same envelope so the frontend can rely on a single response shape: ```ts // success res.status(200).json({ success: true, data: , message?: 'Optional human message', }); // error (always via next(err) → errorHandler middleware) res.status(err.status || 500).json({ success: false, error: err.code || 'INTERNAL_ERROR', message: err.message, details?: err.details, }); ``` ### Error handler pattern Throw typed errors and let `src/shared/middleware/errorHandler.ts` catch them: ```ts // Controllers / services class HttpError extends Error { constructor(public status: number, public code: string, message: string, public details?: unknown) { super(message); } } throw new HttpError(404, 'REQUEST_NOT_FOUND', 'Purchase request not found'); ``` Wrap async route handlers so rejected promises reach `next()`: ```ts const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); router.get('/:id', asyncHandler(controller.getById)); ``` ### Logging Use `src/utils/logger.ts`: ```ts import { log, logError } from "src/utils/logger"; log(`✅ Payment ${id} confirmed`); logError("Request Network webhook verification failed", err); ``` Never use raw `console.error` in service code — it bypasses Sentry breadcrumbs. ### Database access PostgreSQL + Drizzle ORM is the **only** database layer. MongoDB and Mongoose have been completely removed from the runtime. Rules: - Always access data through the repository layer (`src/db/repositories/`). Call `getXxxRepo()` from the factory (`src/db/repositories/factory.ts`). - Never import `mongoose` or reference Mongoose models — they no longer exist. All `src/models/` Mongoose model files have been deleted. - Never use raw Drizzle `db` queries in service or controller code; wrap them in a repository method. - `PG_URL` is a required environment variable. The old `MONGO_URI` / `MONGODB_URI` / `MONGO_CONNECT_MODE` vars are obsolete and must not be added back. ```ts // ✅ Correct import { getOfferRepo } from "@db/repositories/factory"; const repo = getOfferRepo(); const offer = await repo.findById(offerId); // ❌ Wrong — Mongoose is gone import { Offer } from "@models/offer"; const offer = await Offer.findById(offerId); ``` ### ID conventions All primary keys are **PostgreSQL UUIDs** (`string`). - Use `.id` to read an entity's primary key — never `._id`. - The `users` table retains a `legacy_object_id` column (the old MongoDB ObjectId string) for backward compatibility only. Do not use `legacy_object_id` in new code; use `user.pgId` (UUID) for foreign-key references to users (e.g. `offer.sellerId`). - Marketplace FKs such as `offer.sellerId` are `user.pgId` (UUID), **not** `user._id` (legacy ObjectId). ```ts // ✅ Correct const id: string = entity.id; // Postgres UUID // ❌ Wrong — _id is a legacy ObjectId string, not a Postgres UUID const id = entity._id; ``` --- ## 5. Frontend — UI standards **Authoritative source: `backend/.cursor/rules/ui-development-standards.mdc`.** Summary below. ### Component file layout For non-trivial components, use a folder: ``` src/components/request-card/ ├── index.ts # Barrel: export * from './component' ├── component.tsx # The React component ├── classes.ts # Styled classes or sx fragments └── types.ts # Props type definitions ``` For one-file atoms, a single `name.tsx` is fine. ### MUI sx-prop — array syntax The codebase uses the **array form** of the `sx` prop everywhere, because it composes cleanly with the spread-from-parent pattern: ```tsx ({ [theme.breakpoints.up('md')]: { flexDirection: 'row' } }), isActive && { backgroundColor: 'primary.main' }, ...(Array.isArray(sx) ? sx : [sx]), ]} {...other} /> ``` Reasons: 1. Conditional styles compose naturally (`condition && {...}` is ignored when false). 2. Theme callbacks are first-class items in the array. 3. The trailing spread allows parent overrides without prop drilling. ### No inline colors — use the theme ```tsx // ❌ Wrong sx={{ color: '#00A76F', bgcolor: '#FFF' }} // ✅ Right sx={(theme) => ({ color: theme.vars.palette.primary.main, bgcolor: 'background.paper', })} ``` Use `theme.vars.palette` (CSS variables) — automatically dark/light aware. ### Forms — React Hook Form + Zod All forms use `react-hook-form` with `zodResolver` and the `RHF*` components from `src/components/hook-form`: ```tsx const schema = z.object({ email: z.string().email('Invalid email'), amount: z.number().min(1), }); const methods = useForm({ resolver: zodResolver(schema), defaultValues: { email: '', amount: 0 }, });
``` ### Icons — Iconify only There is exactly one icon component: ```tsx import { Iconify } from 'src/components/iconify'; ``` Do **not** introduce another icon library (no `lucide-react`, no `@mui/icons-material`). Iconify covers everything via its registered icon sets. ### Hooks - File: `use-.ts`. Component name: `useCamelCase`. - One hook per file. If a hook needs sub-helpers, colocate them in the same file. - Custom hooks live in `src/hooks/` if generic, otherwise next to the feature. ### Accessibility & responsiveness Mandatory checks: - Touch targets ≥ 44px. - Keyboard focus visible. - Color contrast meets WCAG AA (theme already conforms). - Use `Container` for page-level padding and the breakpoint system (`theme.breakpoints.up('md')`) for layouts. --- ## 6. Commit conventions Authoritative file: `frontend/.commitlintrc.json` (extends `@commitlint/config-conventional`). Backend follows the same convention informally — the AI versioning script (`scripts/ai-enhanced.sh`) reads commit messages to decide on a major/minor/patch bump. ### Allowed types `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci`, `build`, `revert` ### Rules enforced - Type must be present, lower-case. - Subject must be present, lower-case. - Header (`type(scope): subject`) max 100 chars. ### Examples ``` feat: add seller payout history table fix(payment): handle missing transaction hash from shkeeper docs: clarify env vars table refactor(auth): extract jwt refresh into helper chore: bump dependencies feat!: breaking change to api response shape ``` ### Breaking changes Append `!` after the type or include `BREAKING CHANGE:` in the body. This triggers a major-version bump in the auto-version script. See [[Git Workflow#versioning]]. ### Skip a version bump Include `[skip-version]` anywhere in the message — the AI script will recognise it and skip the bump. --- ## 7. Testing standards - All new code should be reachable by at least one test. See [[Testing]]. - Test file naming: `*.test.ts(x)` for unit/integration, `*.spec.ts` for Playwright. - Place new backend tests in `__tests__/` — Jest discovers them via `**/__tests__/**/*.test.ts`. - Place new frontend tests in `__tests__/-test/` or colocate `Component.test.tsx` next to the component. --- ## 8. PR review checklist Before requesting review: - [ ] Lint passes (`yarn lint` / `npm run lint`) - [ ] Typecheck passes (`npm run typecheck`) - [ ] Relevant tests added / updated - [ ] No `console.log` in shipped code (frontend uses `src/utils/logger.ts`) - [ ] No new icon library introduced (Iconify only) - [ ] No inline hex colors (theme only) - [ ] Import order obeys the linter - [ ] Commit messages follow the convention - [ ] If touching env vars, [[Environment Variables]] is updated - [ ] If adding scripts, [[Scripts]] is updated --- ## 9. Banned patterns | Don't | Do | |-------|-----| | `any` in new code | derive a precise type, fall back to `unknown` + narrowing | | `console.log` outside scripts | `log(...)` from `utils/logger` | | Deep relative imports (`../../../foo`) | path aliases (`@shared/foo` backend, `src/...` frontend) | | Inline `style={{ color: '#fff' }}` | `sx` prop with theme tokens | | `@mui/icons-material`, `react-icons`, `lucide-react` | `Iconify` | | `useState` for global state that 3+ components need | a context in `src/contexts/` or a custom hook | | Direct `axios.create` calls in components | use `src/lib/axios.ts` or an action in `src/actions/` | | Hard-coded URLs | constants in `src/routes/paths.ts` (frontend) or env vars (backend) | | Schema changes without a migration | add a Drizzle migration (`drizzle-kit generate`) and document it | | `import mongoose` / Mongoose models | `getXxxRepo()` from `src/db/repositories/factory` | | `entity._id` for Postgres entities | `entity.id` (UUID string) | | `MONGO_URI` / `MONGO_CONNECT_MODE` env vars | `PG_URL` (required) |