Skip to main content

Engineering Standards

TypeScript

Rules

// tsconfig.json — required settings
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true
}
}
  • No any in domain code — use unknown and narrow with Zod or type guards
  • No non-null assertion (!) without an inline comment explaining why it's safe
  • No as Type casts without narrowing first (or document the reason)
  • Every exported function has an explicit return type
  • Enum values are SCREAMING_SNAKE_CASE; normal variables are camelCase

Package Structure

packages/
@salesarc/types ← Shared TypeScript interfaces, Zod schemas
@salesarc/db ← Prisma client, migrations, seeds
@salesarc/logging ← Structured logger, PII redaction
@salesarc/reward-engine ← Pure business logic, no I/O

apps/
api/ ← Express app, routes, middleware
web-consumer/ ← Consumer React app
web-client/ ← Merchant portal React app
web-admin/ ← Admin console React app

Domain logic in packages, not in controllers. Controllers should be thin I/O boundaries — validate input, call a service, return the result.

// ✅ Good — thin controller
router.post('/earn', validateBody(earnSchema), async (req, res) => {
const result = await rewardEngine.evaluate(req.tenantId, req.body);
res.json(result);
});

// ❌ Bad — business logic in route handler
router.post('/earn', async (req, res) => {
const { amount, tenantId } = req.body;
const rules = await db.rewardRules.findMany({ where: { tenantId, isActive: true } });
let points = 0;
for (const rule of rules) {
if (amount >= rule.minAmount) points += Math.floor(amount / 100) * rule.value;
}
// ... more business logic ...
});

Git Workflow

Branch Naming

feature/<short-description>     ← New capabilities
fix/<short-description> ← Bug fixes
chore/<short-description> ← Tooling, dependencies, config
docs/<short-description> ← Documentation only
refactor/<short-description> ← Code restructuring, no behavior change

Examples:

feature/wallet-redemption-expiry
fix/square-amount-parsing-cents
chore/bump-prisma-5.14
docs/add-runbook-token-refresh

Commit Messages — Conventional Commits

Format: <type>(<scope>): <short imperative description>

feat(reward-engine): add day-of-week multiplier support
fix(pos-adapter): parse Square amounts as cents not dollars
chore(deps): bump @prisma/client to 5.14.0
test(wallet): add concurrent deduction integration test
docs(api): add redemption endpoint examples
refactor(auth): extract OTP expiry logic to service layer
  • Present tense, imperative: "add", not "added" or "adds"
  • Scope is the package or area being changed
  • Breaking changes get a ! suffix: feat(api)!: change wallet balance response shape

Code Review Standards

PR Size

  • Target < 400 lines changed per PR
  • If a feature is large, split into: schema migration → service layer → API endpoints → frontend (separate PRs)
  • Reviewers should be able to review in < 30 minutes

PR Description Template

## What
Brief description of what this changes.

## Why
Context for why this change is needed.

## Testing
How was this tested? List test cases added.

## Screenshots (if UI)
Before/after screenshots.

## Checklist
- [ ] Unit tests added/updated
- [ ] Integration test for new behavior
- [ ] No `any` introduced
- [ ] Migration backwards-compatible (if DB change)

Review Expectations

  • Reviewer must understand what the code does (not just "LGTM")
  • At least one approving review required before merge
  • Author resolves all comments or acknowledges disagreement before merging
  • CI must be green: type check, lint, all tests

Database Migrations

  • All schema changes via migration files — never apply changes directly to production DB
  • Migrations must be backwards-compatible during deployment window (old code + new schema must work)
  • Additive changes first (add column nullable) → deploy → make required in code → deploy → add constraint
# Generate migration from schema change
npx prisma migrate dev --name add_wallet_frozen_status

# Apply to production (CI)
npx prisma migrate deploy

Error Handling

All errors returned by the API use a consistent shape:

{
"code": "INSUFFICIENT_BALANCE",
"message": "Wallet balance is 250 points; requested 500 points",
"requestId": "req_01HZXYZ"
}

Never leak stack traces or internal error details to the client.

// Error handler middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const statusCode = err instanceof AppError ? err.statusCode : 500;
const code = err instanceof AppError ? err.code : 'INTERNAL_ERROR';

logger.error('unhandled.error', {
error: err.message,
stack: err.stack,
correlationId: req.correlationId,
});

res.status(statusCode).json({
code,
message: statusCode === 500 ? 'An unexpected error occurred' : err.message,
requestId: req.correlationId,
});
});

Documentation Standards

  • Every packages/* package must have a README.md explaining purpose, usage, and key exports
  • Complex algorithms (rule evaluation, ledger math) get inline comments explaining the invariants
  • Public API endpoints have JSDoc comments or inline OpenAPI annotations
  • Keep docs close to code — update docs in the same PR as the code change

Naming Conventions

ConceptConventionExample
Fileskebab-case.tsreward-engine.ts
ClassesPascalCaseWalletService
FunctionscamelCaseevaluateRule()
VariablescamelCasetenantId, walletBalance
ConstantsSCREAMING_SNAKE_CASEMAX_OTP_ATTEMPTS
Database tablessnake_casewallet_ledger
API endpointskebab-case/consumer/wallets/:id
Env varsSCREAMING_SNAKE_CASEDATABASE_URL
Written byDhruv Doshi