Skip to content

Function Patterns

Consistent function patterns improve code readability and maintainability.

Module Exports

Prefer Named Exports

Avoid export default except for configuration files (e.g., eslint.config.mjs) or lazy-loaded components. Use named exports for all classes, functions, and constants.

Why?

  • Refactoring: Renaming a symbol automatically updates imports across the monorepo
  • Consistency: Enforces consistent naming across all files importing the module
typescript
// ✅ GOOD
export class UserController { }
export function createUser() { }
export const DEFAULT_OPTIONS = { };

// ❌ BAD
export default class UserController { }

The Options Object Pattern

Prefer using a single object parameter (opts) over multiple positional arguments, especially for constructors and public methods with more than 2 arguments.

Why?

  • Extensibility: You can add new properties without breaking existing calls
  • Readability: Named keys act as documentation at the call site
typescript
// ✅ GOOD
class UserService {
  createUser(opts: { name: string; email: string; role?: string }) {
    // ...
  }
}
// Usage: service.createUser({ name: 'John', email: 'john@example.com' });

// ❌ BAD
class UserService {
  createUser(name: string, email: string, role?: string) {
    // ...
  }
}
// Usage: service.createUser('John', 'john@example.com');

Function Naming Conventions

Use consistent prefixes based on function purpose:

PrefixPurposeExamples
generate*Create column definitions / schemasgenerateIdColumnDefs(), generateTzColumnDefs()
build*Construct complex objectsbuildPrimitiveCondition(), buildJsonOrderBy()
to*Convert/transform datatoCamel(), toBoolean(), toStringDecimal()
is*Boolean validation/checkisWeekday(), isInt(), isFloat(), isPromiseLike()
extract*Pull out specific partsextractTimestamp(), extractWorkerId(), extractSequence()
enrich*Enhance with additional dataenrichUserAudit(), enrichWithMetadata()
get*Retrieve/fetch datagetSchema(), getConnector(), getError()
resolve*Determine/compute valueresolveValue(), resolvePath()

Examples:

typescript
// Generators - create schema definitions
const idCols = generateIdColumnDefs({ id: { dataType: 'string' } });
const tzCols = generateTzColumnDefs();

// Builders - construct complex query objects
const condition = buildPrimitiveCondition(column, operator, value);
const orderBy = buildJsonOrderBy(schema, path, direction);

// Converters - transform data types
const camelCase = toCamel('snake_case');
const bool = toBoolean('true');
const decimal = toStringDecimal(123.456, 2);

// Validators - boolean checks
if (isWeekday(date)) { /* ... */ }
if (isInt(value)) { /* ... */ }
if (isPromiseLike(result)) { /* ... */ }

// Extractors - pull specific data
const timestamp = extractTimestamp(snowflakeId);
const workerId = extractWorkerId(snowflakeId);

Scope Naming

Every class extending a base class should set its scope using ClassName.name:

typescript
export class JWTTokenService extends BaseService {
  constructor() {
    super({ scope: JWTTokenService.name });
  }
}

export class UserController extends BaseController {
  constructor() {
    super({ scope: UserController.name });
  }
}

Performance Logging Pattern

Use performance.now() for timing critical operations with method-scoped logging:

typescript
async syncData() {
  const t = performance.now();
  this.logger.for('syncData').info('START | Syncing data...');

  // ... operation to measure ...

  this.logger.for('syncData').info('DONE | Took: %s (ms)', performance.now() - t);
}

With the helper utility:

typescript
import { executeWithPerformanceMeasure } from '@venizia/ignis';

await executeWithPerformanceMeasure({
  logger: this.logger.for('syncData'),
  scope: 'DataSync',
  description: 'Sync user records',
  task: async () => {
    await syncAllUsers();
  },
});
// Logs: [DataSync] Sync user records | Took: 1234.56 (ms)

Method-scoped logging pattern:

typescript
class UserService {
  private logger = Logger.get('UserService');

  async createUser(data: CreateUserDto) {
    // Use .for() to add method context to all logs
    this.logger.for('createUser').info('Creating user: %j', data);
    // Output: [UserService-createUser] Creating user: {...}

    try {
      const user = await this.userRepo.create({ data });
      this.logger.for('createUser').info('User created: %s', user.id);
      return user;
    } catch (error) {
      this.logger.for('createUser').error('Failed: %s', error);
      throw error;
    }
  }
}

See Also