Engineering Standards
TypeScript
Rules
// tsconfig.json — required settings
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true
}
}
- No
anyin domain code — useunknownand narrow with Zod or type guards - No non-null assertion (
!) without an inline comment explaining why it's safe - No
as Typecasts without narrowing first (or document the reason) - Every exported function has an explicit return type
- Enum values are
SCREAMING_SNAKE_CASE; normal variables arecamelCase
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 aREADME.mdexplaining 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
| Concept | Convention | Example |
|---|---|---|
| Files | kebab-case.ts | reward-engine.ts |
| Classes | PascalCase | WalletService |
| Functions | camelCase | evaluateRule() |
| Variables | camelCase | tenantId, walletBalance |
| Constants | SCREAMING_SNAKE_CASE | MAX_OTP_ATTEMPTS |
| Database tables | snake_case | wallet_ledger |
| API endpoints | kebab-case | /consumer/wallets/:id |
| Env vars | SCREAMING_SNAKE_CASE | DATABASE_URL |
Written byDhruv Doshi