420 lines
14 KiB
Markdown
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) |
|