Skip to content

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:

  1. TypeScript compiler - strict: true, noUncheckedIndexedAccess: true
  2. ESLint - Custom rules for any, unknown, as unknown as
  3. Build pipeline - Won't compile with violations
  4. 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
anyExplicit interface or Zod schema
as unknown asType guard or Zod validation
unknown (unnarrowed)Narrow with typeof/instanceof/Zod
console.log()logger.info()
throw new Error()throw new NotFoundError()
value || defaultvalue ?? default
.substring().slice()
if (value)if (value !== undefined && value !== null)
if (array.length)if (array.length > 0)
value == nullvalue === null (unless checking both null/undefined)
response.data.fieldconst data = response.data as Type; data.field
Promise.resolve(value) in asyncJust 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

TendSocial Documentation