TendSocial Backend - Zero Tolerance Rules
⚠️ CRITICAL - READ FIRST
These rules have ZERO TOLERANCE. Violating any of these will break the build or cause runtime errors.
Type Safety Rules
❌ NEVER USE any
typescript
// ❌ FORBIDDEN
function process(data: any) { }
const result: any = await fetch();
const config = obj as any;
// ✅ REQUIRED - Define explicit types
interface ProcessData {
id: string;
value: number;
}
function process(data: ProcessData) { }
// ✅ REQUIRED - Use Zod for runtime validation
const ConfigSchema = z.object({
apiKey: z.string(),
timeout: z.number(),
});
type Config = z.infer<typeof ConfigSchema>;❌ NEVER USE as unknown as
typescript
// ❌ FORBIDDEN
const data = response as unknown as MyType;
const value = obj.field as unknown as string;
// ✅ REQUIRED - Use proper type guards or Zod
function isMyType(data: unknown): data is MyType {
return typeof data === 'object' && data !== null && 'id' in data;
}
if (isMyType(response)) {
// data is now MyType
}
// ✅ REQUIRED - Use Zod for external data
const result = MyTypeSchema.safeParse(response);
if (result.success) {
const data: MyType = result.data;
}❌ NEVER USE unknown as a cop-out
typescript
// ❌ FORBIDDEN - unknown without narrowing
function handle(data: unknown) {
console.log(data); // What is this?
}
// ✅ REQUIRED - Narrow immediately
function handle(data: unknown) {
if (typeof data !== 'object' || data === null) {
throw new ValidationError('Expected object');
}
const result = MySchema.safeParse(data);
if (!result.success) {
throw new ValidationError('Invalid data');
}
// Now work with typed data
const typedData: MyType = result.data;
}✅ REQUIRED - Explicit Types Always
typescript
// ❌ FORBIDDEN - Inline types
function createUser(data: { name: string; email: string }) { }
// ✅ REQUIRED - Named types in /types/
// In apps/backend/src/types/services.ts
export interface CreateUserData {
name: string;
email: string;
}
// In service
import type { CreateUserData } from '@/types/services.js';
function createUser(data: CreateUserData) { }✅ REQUIRED - All Types in apps/backend/src/types/
apps/backend/src/types/
├── index.ts # Barrel exports
├── common.ts # JsonValue, JsonObject, etc.
├── enums.ts # Platform, PostStatus, etc.
├── ai.ts # AI-related types
├── platforms.ts # Platform adapter types
├── analytics.ts # Analytics types
├── services.ts # Service layer types
└── ...Logging Rules
❌ NEVER USE console methods
typescript
// ❌ FORBIDDEN
console.log('User created');
console.error('Error occurred');
console.warn('Warning');
console.debug('Debug info');
// ✅ REQUIRED - Use Pino loggers ONLY
import { logger, platformLogger, aiLogger } from '@/infra/logger.js';
logger.info({ userId }, 'User created');
logger.error({ err: error }, 'Error occurred');
logger.warn({ metric }, 'Warning');
logger.debug({ cacheKey }, 'Debug info');✅ REQUIRED - Structured Logging
typescript
// ❌ FORBIDDEN
logger.info('User ' + userId + ' performed ' + action);
logger.error('Error: ' + error.message);
// ✅ REQUIRED
logger.info({ userId, action }, 'User performed action');
logger.error({ err: error, userId, action }, 'Operation failed');Error Handling Rules
❌ NEVER USE generic Error
typescript
// ❌ FORBIDDEN
throw new Error('User not found');
throw new Error('Invalid input');
// ✅ REQUIRED - Use AppError hierarchy
import { NotFoundError, ValidationError } from '@/infra/errors.js';
throw new NotFoundError('User', userId);
throw new ValidationError('Email is required', 'email');❌ NEVER swallow errors
typescript
// ❌ FORBIDDEN
try {
await operation();
} catch {
// Silent failure
}
try {
await operation();
} catch (error) {
// Logged but not handled
logger.error('Failed');
}
// ✅ REQUIRED - Log and re-throw or handle
import { logError } from '@/infra/logger.js';
import { AppError } from '@/infra/errors.js';
try {
await operation();
} catch (error) {
logError(logger, error instanceof Error ? error : new Error(String(error)), 'Operation failed');
throw AppError.from(error); // Re-throw as AppError
}✅ REQUIRED - Use neverthrow for composition
typescript
// ✅ Use Result<T, E> for fallible operations
import { ResultAsync, ok, err } from '@/infra/result.js';
import { NotFoundError, AppError } from '@/infra/errors.js';
function getUser(id: string): ResultAsync<User, NotFoundError> {
return ResultAsync.fromPromise(
db.user.findUnique({ where: { id } }),
(error) => AppError.from(error)
).andThen((user) => {
if (!user) {
return err(new NotFoundError('User', id));
}
return ok(user);
});
}ESM Rules
✅ REQUIRED - .js extensions on relative imports
typescript
// ❌ FORBIDDEN
import { helper } from './utils';
import { config } from '../config';
// ✅ REQUIRED
import { helper } from './utils.js';
import { config } from '../config.js';
// ✅ Node built-ins and npm packages don't need .js
import crypto from 'crypto';
import { z } from 'zod';❌ NEVER USE require()
typescript
// ❌ FORBIDDEN - This is ESM, not CommonJS
const prisma = require('./prisma');
// ✅ REQUIRED
import prisma from './prisma.js';Multi-Tenant Rules
✅ REQUIRED - Use getTenantPrisma for tenant data
typescript
// ❌ FORBIDDEN - Data leak risk
import prisma from '@/infra/prisma.js';
const posts = await prisma.post.findMany(); // ALL companies!
// ✅ REQUIRED
import { getTenantPrisma } from '@/infra/prisma.js';
async (request, reply) => {
const { companyId } = request.user;
const tenantPrisma = getTenantPrisma(companyId);
const posts = await tenantPrisma.post.findMany(); // Auto-filtered
}Enforcement
These rules are enforced by:
- TypeScript compiler -
strict: true,noUncheckedIndexedAccess: true - ESLint - Custom rules for
any,unknown,as unknown as - Build pipeline - Won't compile with violations
- Code review - Automated checks on PRs
Common Recurring Mistakes (ALSO ZERO TOLERANCE)
These patterns keep appearing and need to be fixed. Learn these NOW:
❌ Using || instead of ??
typescript
// ❌ WRONG - treats 0, '', false as falsy
const port = config.port || 3000;
const name = user.name || 'Unknown';
// ✅ CORRECT - only null/undefined trigger default
const port = config.port ?? 3000;
const name = user.name ?? 'Unknown';❌ Using .substring() instead of .slice()
typescript
// ❌ WRONG - substring is deprecated pattern
const token = authHeader.substring(7);
// ✅ CORRECT - use slice
const token = authHeader.slice(7);❌ Wrong response.data typing pattern
typescript
// ❌ WRONG - accessing properties before typing
const userId = response.data.id;
const token = response.data.access_token;
// ✅ CORRECT - type the whole response.data first
const data = response.data as { id: string; access_token: string };
const userId = data.id;
const token = data.access_token;❌ Fake async functions
typescript
// ❌ WRONG - returns Promise.resolve() unnecessarily
async getCapabilities(): Promise<string[]> {
return Promise.resolve(['PUBLISH', 'DELETE']);
}
// ✅ CORRECT - just return the value or add real async work
async getCapabilities(): Promise<string[]> {
await Promise.resolve(); // If interface requires async
return ['PUBLISH', 'DELETE'];
}
// ✅ BETTER - return directly if possible
getCapabilities(): string[] {
return ['PUBLISH', 'DELETE'];
}❌ Loose truthiness checks
typescript
// ❌ WRONG - these treat '', 0, false as falsy
if (value) { }
if (array.length) { }
if (string) { }
// ✅ CORRECT - explicit checks
if (value !== undefined && value !== null) { }
if (array.length > 0) { }
if (string !== undefined && string.length > 0) { }❌ Using == instead of ===
typescript
// ❌ WRONG - loose equality
if (value == null) { }
if (count == 0) { }
// ✅ CORRECT - strict equality
if (value === null) { }
if (value === undefined) { }
if (count === 0) { }
// ✅ ACCEPTABLE - checking both null and undefined
if (value == null) { } // Only for null OR undefined check❌ Broad ESLint disables
typescript
// ❌ WRONG - disables entire rule for whole file
/* eslint-disable @typescript-eslint/no-explicit-any */
// ❌ WRONG - disables multiple rules unnecessarily
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
// ✅ CORRECT - specific disable on specific line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private getErrorMessage(error: any): string {❌ Not destructuring before using
typescript
// ❌ WRONG - repetitive and harder to type
const name = response.data.name;
const email = response.data.email;
const age = response.data.age;
// ✅ CORRECT - destructure for clarity
const { name, email, age } = response.data;❌ Array checks with ||
typescript
// ❌ WRONG
const items = response.data.items || [];
const users = apiResponse.users || [];
// ✅ CORRECT
const items = response.data.items ?? [];
const users = apiResponse.users ?? [];❌ Missing index signatures when needed
typescript
// ❌ WRONG - can't assign to Prisma.InputJsonValue
interface Metadata {
title: string;
description: string;
}
// ✅ CORRECT - add index signature for JSON storage
interface Metadata {
title: string;
description: string;
[key: string]: JsonValue; // Allows additional properties
}❌ Not using tenant-scoped Prisma
typescript
// ❌ WRONG - data leak!
import prisma from '@/infra/prisma.js';
async function getPosts() {
return prisma.post.findMany(); // Returns ALL companies' posts!
}
// ✅ CORRECT
import { getTenantPrisma } from '@/infra/prisma.js';
async function getPosts(companyId: string) {
const tenantPrisma = getTenantPrisma(companyId);
return tenantPrisma.post.findMany(); // Auto-filtered by companyId
}❌ Importing from wrong locations
typescript
// ❌ WRONG - importing from generated Prisma client directly
import { Prisma } from '../generated/prisma/client.js';
// ✅ CORRECT - import from specific type file (better for tree-shaking)
import { type Prisma } from '../generated/prisma/client.js';
// OR if using types barrel for interfaces only:
import type { Prisma } from '@/types/prisma.js';❌ Missing .js extensions on local imports
typescript
// ❌ WRONG - ESM requires extensions
import { helper } from './utils';
import { User } from '../types/user';
// ✅ CORRECT
import { helper } from './utils.js';
import { User } from '../types/user.js';❌ Incorrect async adapter method signatures
typescript
// ❌ WRONG - not awaiting in async function
async validateMedia(file: Buffer): Promise<ValidationResult> {
const errors: string[] = [];
if (file.length > MAX_SIZE) {
errors.push('Too large');
}
return Promise.resolve({ valid: errors.length === 0 }); // Unnecessary
}
// ✅ CORRECT
async validateMedia(file: Buffer): Promise<ValidationResult> {
await Promise.resolve(); // Satisfy async requirement
const errors: string[] = [];
if (file.length > MAX_SIZE) {
errors.push('Too large');
}
return { valid: errors.length === 0 }; // Direct return
}❌ Not handling errors in catch blocks properly
typescript
// ❌ WRONG - generic error handling
catch (error) {
throw new Error('Operation failed'); // Lost context!
}
// ❌ WRONG - using any without eslint-disable
catch (error) {
return error.message; // TypeScript error
}
// ✅ CORRECT - proper error conversion
catch (error) {
throw AppError.from(error, { context: 'operation' });
}
// ✅ CORRECT - when you need any for error handling
// eslint-disable-next-line @typescript-eslint/no-explicit-any
catch (error: any) {
const message = error?.message ?? String(error);
throw new AppError(message);
}Violation Response
If you see code that violates these rules:
🚨 ZERO TOLERANCE VIOLATION
File: [filename]
Line: [line number]
Violation: [any / as unknown as / console.log / etc.]
This MUST be fixed immediately. The correct pattern is:
[Show compliant code]Quick Reference Card
| ❌ FORBIDDEN | ✅ REQUIRED |
|---|---|
any | Explicit interface or Zod schema |
as unknown as | Type guard or Zod validation |
unknown (unnarrowed) | Narrow with typeof/instanceof/Zod |
console.log() | logger.info() |
throw new Error() | throw new NotFoundError() |
value || default | value ?? default |
.substring() | .slice() |
if (value) | if (value !== undefined && value !== null) |
if (array.length) | if (array.length > 0) |
value == null | value === null (unless checking both null/undefined) |
response.data.field | const data = response.data as Type; data.field |
Promise.resolve(value) in async | Just return value or await Promise.resolve() |
import x from './file' | import x from './file.js' |
prisma.post.findMany() | getTenantPrisma(companyId).post.findMany() |
/* eslint-disable rule */ | // eslint-disable-next-line rule |
import from '../generated/prisma' | import from '@/types/index.js' (for types) |
import { Platform } from '@/types/index.js' | import { Platform } from '@/types/enums.js' (for runtime values) |
AppError.from() | AppError.from() (during migration) |
logError() | logError() (during migration) |
Documentation
- Logging Guide - Comprehensive Pino patterns
- Error Handling Guide - neverthrow and AppError patterns
- Internal Architecture - System design
- Coding Standards - Additional code style guidelines