Files
nick-doc/07 - Development/Coding Standards.md

420 lines
14 KiB
Markdown

---
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 | `<verb><Noun>` | `getRequestById`, `createOffer` |
| Express route file | `<domain>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: <payload>,
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
<Box
sx={[
{ p: 3, borderRadius: 2, bgcolor: 'background.paper' },
(theme) => ({ [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 },
});
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<RHFTextField name="email" label="Email" />
<RHFTextField name="amount" type="number" label="Amount" />
</form>
</FormProvider>
```
### Icons — Iconify only
There is exactly one icon component:
```tsx
import { Iconify } from 'src/components/iconify';
<Iconify icon="eva:home-fill" />
<Iconify icon="solar:user-outline" width={24} height={24} />
```
Do **not** introduce another icon library (no `lucide-react`, no `@mui/icons-material`). Iconify covers everything via its registered icon sets.
### Hooks
- File: `use-<kebab>.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__/<domain>-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) |