Initial commit: nick docs
This commit is contained in:
380
07 - Development/Coding Standards.md
Normal file
380
07 - Development/Coding Standards.md
Normal file
@@ -0,0 +1,380 @@
|
||||
---
|
||||
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` |
|
||||
| Mongoose model | PascalCase singular | `User`, `PurchaseRequest` |
|
||||
| Mongo collection | lowercase plural (auto) | `users`, `purchaserequests` |
|
||||
| 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 — talks to models, throws domain errors
|
||||
└── marketplaceRepository.ts # Optional Mongoose query helpers (when service grows)
|
||||
```
|
||||
|
||||
### 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("SHKeeper webhook verification failed", err);
|
||||
```
|
||||
|
||||
Never use raw `console.error` in service code — it bypasses Sentry breadcrumbs.
|
||||
|
||||
---
|
||||
|
||||
## 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 migration script in `src/scripts/` and document it |
|
||||
Reference in New Issue
Block a user