Skip to main content

Style Guide

This guide defines all coding standards, architectural principles, and workflow practices for Yew Search. These rules exist to keep the codebase consistent as the team grows.

Code Style

Always Use Curly Braces

Even for single-line statements. Makes code easier to extend and debug.

// Bad
if (!user) return;

// Good
if (!user) {
return;
}

Arrow Functions Need Explicit Returns

No implicit returns. Makes debugging easier.

// Bad
const doubled = numbers.map(n => n * 2);

// Good
const doubled = numbers.map((n) => {
return n * 2;
});

One Operation Per Line

Don't chain operations. Each line does one thing.

// Bad
const result = await fetch(url).then(parse).catch(handleError);

// Good
const response = await fetch(url);
const parsed = await parse(response);
const result = handleError(parsed);

Use Descriptive Variable Names

No abbreviations. Code is read more than written.

// Bad
const usr = await db.find(id);
const fn = usr.getName();

// Good
const user = await db.find(id);
const fullName = user.getName();

Exceptions: i, j, k for loop counters, req/res in Express, err/error for errors

Naming Conventions

  • Files/Folders: kebab-case lowercase (user-service.ts, auth/)
  • Classes: PascalCase (UserService, EmailController)
  • Interfaces: PascalCase (User, Integration)
  • Methods/Variables: camelCase (createUser, emailAddress)
  • Constants: SCREAMING_SNAKE_CASE (MAX_RETRIES, API_VERSION)

Import Organization

Group imports with blank lines between groups:

// 1. Node.js built-ins
import { join } from 'path';

// 2. External packages
import { Injectable } from '@nestjs/common';

// 3. Internal modules
import { UserService } from '../user/user.service';

TypeScript Rules

  • Explicit return types on public methods
  • readonly for injected dependencies
  • interface over type for objects
  • const by default, let only when needed

File Structure

No Barrel Files

Don't create index.ts files that re-export everything. They make refactoring harder and imports inconsistent.

// Bad - src/user/index.ts
export * from './user.service';
export * from './user.controller';
export * from './user.entity';

// Good - import directly
import { UserService } from '../user/user.service';

Database Design

No Arrays in Records (Exception: Denormalization)

Use join tables instead of arrays. Arrays make querying and foreign keys impossible.

// Bad
interface User {
id: string;
roleIds: string[]; // Can't enforce foreign keys
}

// Good - join table
interface User {
id: string;
}

interface UserRole {
id: string;
userId: string; // Foreign key
roleId: string; // Foreign key
}

Exception: After creating a join table, you can denormalize IDs into an array for performance but this should be a very deliberate choice:

interface User {
id: string;
roleIds: string[]; // Denormalized from user_role table
}

The join table remains the source of truth.

Cascade Foreign Keys

Child records maintain foreign keys to all ancestors:

Table A: id
Table B: id, a_id
Table C: id, a_id, b_id (references both A and B)

Always Use Soft Deletes

Never hard delete. Use deletedAt timestamp or isDeleted boolean.

@DeleteDateColumn()
public deletedAt: Date | null;

// Unique indexes must exclude soft-deleted records
@Index(['email'], { unique: true, where: '"deleted_at" IS NULL' })

API Design

APIs Are Generic, Not Frontend-Specific

Design APIs as if they're public third-party services. Don't couple to frontend UI.

Exception: Analytics endpoints can be specific, but keep them as generic as possible.

Filter by Arrays, Not Singles

All filters accept arrays. Use inIds and ninIds (not in / in).

// Bad
GET /users?id=123

// Good
GET /users?inIds=123,456,789
GET /users?ninIds=999

Never Populate Foreign Keys

Don't automatically load related records. Leads to N+1 queries and security issues.

// Bad
{
"user": {
"id": "123",
"team": { ... } // Populated team object
}
}

// Good
{
"user": {
"id": "123",
"teamId": "456" // Just the ID
}
}

Clients make separate requests if they need related data.

Pass Objects, Not Loose Parameters

// Bad
function createUser(name: string, email: string, age: number) { }

// Good
function createUser(params: { name: string; email: string; age: number }) { }

All Responses Extend Base DTOs

Every API response must extend BaseReadOneResponseDTO or BaseReadManyResponseDTO. Provides consistent requestId, timestamp, and duration.

export class UserReadOneResponseDTO extends BaseReadOneResponseDTO {
constructor(value: any) {
super(value);
this.operation = 'user.readOne';
Object.assign(this, value);
}

@ApiProperty({ type: UserResult })
public result: UserResult;
}

Never Break API Compatibility

  • Add new fields freely
  • Never remove fields
  • Deprecate first, remove later (if ever)
  • Breaking changes: once per year maximum, team decision only

Architecture Principles

Self-Hosted Only

No third-party SaaS dependencies. Everything must run on Raspberry Pi 4+.

❌ MongoDB Atlas, AWS SQS, Datadog
✅ PostgreSQL, RabbitMQ, Self-hosted monitoring

Workflow

No Git Hooks

Never block commits with pre-commit hooks. Developers should commit freely.

Why: If a developer's laptop dies after days of work, they lose everything. Collaboration requires pushing broken code to branches.

Instead: CI/CD blocks merges to main if tests/linting fail. Block at merge time, not commit time.

Service Patterns

Always Inject ContextService

Use ContextService to automatically include requestId, userId, traceId, spanId in logs.

@Injectable()
export class UserService {
constructor(
private readonly logger: CustomLoggerService,
private readonly contextService: ContextService,
) {}

async createUser(data: CreateUserInput): Promise<User> {
const context = {
module: UserService.name,
method: 'createUser',
...this.contextService.getLoggingContext(),
};

this.logger.logWithContext('Creating user', context);
// ... rest of method
}
}

Services Throw Domain Errors, Controllers Convert to HTTP

Services don't know about HTTP. Controllers handle HTTP concerns.

// Service
throw new UserNotFoundError(userId);

// Controller
try {
const user = await this.userService.readOne({ id });
return new UserResponseDTO({ result: user });
} catch (error) {
if (error instanceof UserNotFoundError) {
throw new NotFoundException(error.message);
}
throw error;
}

When to Break These Rules

Never:

  • API compatibility
  • Self-hosted requirement
  • Soft deletes
  • No git hooks

Rarely (team decision only):

  • Arrays in database (only for denormalization)
  • Barrel files (only if framework requires it)
  • Frontend-specific APIs (only for analytics)

Judgment call:

  • Variable name length (balance clarity vs brevity)
  • One operation per line (sometimes clarity suffers)

Enforcement

  • Linting: ESLint + Prettier enforce code style (runs in CI/CD)
  • Code Review: Humans catch architectural issues
  • CI/CD: Blocks merges if tests/linting fail
  • Documentation: This guide is the source of truth

When in doubt, consistency > personal preference. Follow the guide even if you disagree.