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
readonlyfor injected dependenciesinterfaceovertypefor objectsconstby default,letonly 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.