---
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) |
