Starisian Technologies — Node.js Server Reference Implementation
This document is the Node.js and server-side JavaScript implementation standard for Starisian Technologies. It governs all backend JavaScript services, API servers, worker processes, and build tooling written in Node.js.
All rules in the Standards Handbook apply in full. This document adds Node.js-specific requirements on top of them.
- Node.js LTS (latest active LTS release — not current, not end-of-life)
- TypeScript 5+ — strict mode required for all services
- Express or Fastify — no other HTTP frameworks without architectural justification
- ESLint +
@typescript-eslint(enforced, not auto-fixed) - Jest (unit/integration testing)
- Playwright (E2E testing where applicable)
- (M) Node.js LTS only — never use a non-LTS or end-of-life release in production
- (M) TypeScript strict mode (
"strict": trueintsconfig.json) - (M)
"type": "module"inpackage.jsonfor new services — ESM only, no CommonJS in new code - (X) Dynamic
require()in production code - (X)
eval()ornew Function()— arbitrary code execution is forbidden
All functions must have typed parameters and return types. any is prohibited without inline justification.
// Required
async function processJob(jobId: string, payload: JobPayload): Promise<JobResult> { ... }
// Forbidden
async function processJob(jobId, payload) { ... } // no types
async function processJob(jobId: any, payload: any): Promise<any> { ... } // any without reason| FAIL | TypeScript file with noImplicitAny violations |
|---|---|
| FAIL | use of any without inline justification comment |
- (M) All configuration via environment variables — no hardcoded values
- (M) Validate all required environment variables at startup; fail immediately with clear error if missing
- (M) Use a dedicated config module — never read
process.envdirectly inside business logic
// Required — validate at startup
type EnvType = 'string' | 'number' | 'url';
function requiredEnv(key: string, type: 'string'): string;
function requiredEnv(key: string, type: 'number'): number;
function requiredEnv(key: string, type: 'url'): string;
function requiredEnv(key: string, type: EnvType): string | number {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
switch (type) {
case 'string':
return value;
case 'number': {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(`Environment variable ${key} must be a valid number`);
}
return parsed;
}
case 'url':
try {
new URL(value);
return value;
} catch {
throw new Error(`Environment variable ${key} must be a valid URL`);
}
}
}
const config = {
port: requiredEnv('PORT', 'number'),
authorityEndpoint: requiredEnv('AUTHORITY_ENDPOINT', 'url'),
dbUrl: requiredEnv('DATABASE_URL', 'string'),
};| FAIL | service that starts without validating required environment variables |
|---|---|
| FAIL | process.env accessed directly in business logic outside config module |
- (M) Parse and validate all request input before processing
- (M) Use a schema validation library (e.g., Zod) for request body, query params, and headers
- (M) Reject malformed input with 400 before it reaches business logic
- (X) Trust client-supplied values without validation
// Required — validate before use
const schema = z.object({
userId: z.string().uuid(),
action: z.enum(['record', 'upload', 'review']),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid request' });
}- (M) All responses return
Content-Typeheader - (M) Error responses use consistent shape:
{ error: string, code?: string } - (M) Stack traces never sent to client
- (X) Unhandled promise rejections — all async route handlers wrapped in error boundary
// Required — async error boundary wrapper
function asyncHandler(fn: RequestHandler): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
router.post('/upload', asyncHandler(async (req, res) => {
// handler body
}));- (M) Request timeout configured at server level — default 30 seconds (generous for 2G/3G clients)
- (M) External service calls (authority layer, DB, cache) use explicit per-call timeouts
- (X) Unbounded external service calls
- (M) Parameterized queries only — no string interpolation
- (M) All queries must have explicit
LIMIT— no unbounded queries - (M) Use an ORM or query builder that enforces parameterization (e.g., Prisma, Knex)
- (M) Connection pooling required — no single connection per request
- (X) Raw SQL string concatenation
// Required — parameterized via query builder
const records = await db('submissions')
.select('id', 'user_id', 'created_at')
.where({ status: 'pending' })
.limit(50);
// Forbidden
const records = await db.raw(`SELECT * FROM submissions WHERE status = '${status}'`);| FAIL | raw SQL string interpolation |
|---|---|
| FAIL | unbounded query without LIMIT |
| FAIL | SELECT * in any query |
- (M) Redis for distributed cache — never in-memory cache shared across processes
- (M) All cache keys namespaced by service and entity type
- (M) All cache entries have explicit TTL
- (M) Write operations invalidate related cache entries immediately
- (X) Using in-memory
Mapor global variable as a shared cache across requests
// Required — namespaced key with TTL
await redis.set(`spx:jobs:${jobId}`, JSON.stringify(result), 'EX', 300);
// Required — invalidate on write
await redis.del(`spx:jobs:${jobId}`);- (M) Long-running operations (> 2 seconds) run in a background queue — never in the request lifecycle
- (M) Job queues use a durable backing store (Redis, DB) — never in-memory only
- (M) Every job defines: retry count (max 3), retry strategy (exponential backoff), timeout
- (M) Failed jobs beyond retry budget move to dead-letter queue
- (X) Fire-and-forget without error handling
// Required — structured job definition
const job: JobDefinition = {
id: generateJobId(),
type: 'transcode-audio',
payload: { uploadId, format: 'opus' },
maxRetries: 3,
timeoutMs: 30_000,
};
await queue.enqueue(job);- (M) Max 1 active governed mutation per user — enforce with distributed lock (Redis)
- (M) All writes use row-level locking or optimistic versioning
- (M) Distributed locks must have TTL to prevent deadlock on failure
// Required — distributed lock with TTL
const lock = await redis.set(
`spx:lock:user:${userId}`,
requestId,
'NX',
'EX',
30 // 30s TTL — prevents indefinite lock on crash
);
if (!lock) {
return res.status(429).json({ error: 'Another operation is in progress' });
}- (M) Structured JSON logging — never raw
console.login production - (M) All log entries include:
timestamp,level,requestId,userId(if applicable),message - (M) Request IDs propagated through all log entries within a request lifecycle
- (M) Errors logged with full context — stack trace to server logs only, generic message to client
- (X)
console.log/console.errorin production code — use structured logger
// Required — structured log entry
logger.info({
requestId,
userId,
action: 'upload.chunk.received',
chunkIndex,
uploadId,
});
// Forbidden
console.log('Got chunk', chunkIndex);- (M) All input validated and sanitized before processing
- (M) Rate limiting enforced at server level (complement to edge-layer limits in the handbook)
- (M) Secrets managed via environment variables — never committed to source code
- (M) CORS configured explicitly — no wildcard
*in production - (M) All authority-layer checks performed before governed actions
- (X) Hardcoded credentials, API keys, or secrets in source or config files
- (X) Trust of client-supplied user IDs, timestamps, or permission claims without authority-layer verification
- (M) Package manager:
pnpm(per ADR-017). Nonpmoryarninvocations in workflows, scripts, READMEs, Dockerfiles, orMakefiles. - (M)
pnpm-lock.yamlcommitted and kept current;package-lock.jsonandyarn.lockMUST NOT exist alongside it. - (M)
package.jsondeclares the pinned pnpm version via thepackageManagerfield (e.g."packageManager": "pnpm@9.x"); Corepack honors this automatically. - (M) CI installs with
pnpm install --frozen-lockfile(strict equivalent ofnpm ci). - (M) Dependency license audit before adding any new package.
- (M) Regular dependency vulnerability scan (
pnpm auditor equivalent) in CI. - (X)
npm install,npm ci,yarn install, or any non-pnpm package-manager invocation in any repo with apackage.json. - (X) Packages with known high/critical CVEs without documented mitigation.
- (X) Packages with no active maintenance and no plan for replacement.
| FAIL | repo contains package-lock.json or yarn.lock (per ADR-017) |
|---|---|
| FAIL | package.json missing packageManager: "pnpm@..." field |
| FAIL | CI workflow invokes npm install, npm ci, or yarn install |
- (M) Jest for unit and integration tests
- (M) All route handlers tested with happy path and error path
- (M) All authority-layer integration points have integration tests
- (M) Test database distinct from development database — never test against production data
- (M)
tsc --noEmitin CI — TypeScript type-check without emitting files - (M) ESLint with
@typescript-eslintrules — enforced in CI, not auto-fixed - (M) Lint failures block merge
- (X) Auto-fix in CI
Version: 2.0 | Starisian Technologies | May 2026
Applies to: All Node.js and server-side JavaScript services governed by Starisian Technologies standards.