14 KiB
title, tags
| title | tags | |
|---|---|---|
| Coding Standards |
|
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):
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: preservefor Next.- All component props must be typed and exported (
export type ComponentNameProps = …). - Prefer
typeoverinterfaceexcept when declaring something that must be extendable from a consumer module. - Re-export from a barrel
index.tsper 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—anyis allowed when you justify it, but failing this rule will be flagged in code review.no-console: off— backend usessrc/utils/logger.ts(a thinconsole.logwrapper) so console statements are fine in scripts. Inside services, preferlog(...)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 (
es5on frontend,allon 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/). CallgetXxxRepo()from the factory (src/db/repositories/factory.ts). - Never import
mongooseor reference Mongoose models — they no longer exist. Allsrc/models/Mongoose model files have been deleted. - Never use raw Drizzle
dbqueries in service or controller code; wrap them in a repository method. PG_URLis a required environment variable. The oldMONGO_URI/MONGODB_URI/MONGO_CONNECT_MODEvars 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
.idto read an entity's primary key — never._id. - The
userstable retains alegacy_object_idcolumn (the old MongoDB ObjectId string) for backward compatibility only. Do not uselegacy_object_idin new code; useuser.pgId(UUID) for foreign-key references to users (e.g.offer.sellerId). - Marketplace FKs such as
offer.sellerIdareuser.pgId(UUID), notuser._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:
- Conditional styles compose naturally (
condition && {...}is ignored when false). - Theme callbacks are first-class items in the array.
- 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
Containerfor 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.tsfor Playwright. - Place new backend tests in
__tests__/— Jest discovers them via**/__tests__/**/*.test.ts. - Place new frontend tests in
__tests__/<domain>-test/or colocateComponent.test.tsxnext 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.login shipped code (frontend usessrc/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) |