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

14 KiB

title, tags
title tags
Coding Standards
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.mdcthat 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):
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: warnany 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:

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:

// 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:

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:

// 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:

// 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():

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:

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.
// ✅ 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).
// ✅ 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:

<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

// ❌ 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:

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:

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)