Disciplines · Reference

Coding Standards

The root tsconfig.base.json defines the baseline TypeScript configuration.

11sections7 minread

On this page

This document defines the coding standards for the Oshun monorepo. These standards are enforced through ESLint, Prettier, TypeScript compiler options, and pre-commit hooks.

Table of Contents#


TypeScript Configuration#

The root tsconfig.base.json defines the baseline TypeScript configuration. All projects extend from it.

Compiler Options#

Option Value Purpose
target ES2022 Modern JavaScript output
module ESNext ESM modules
moduleResolution bundler Bundler-compatible resolution
strict true All strict checks enabled
noImplicitOverride true Requires override keyword
noImplicitReturns true All code paths must return
noFallthroughCasesInSwitch true Switch cases must break/return
noUnusedLocals true No unused local variables
noUnusedParameters true No unused function parameters
isolatedModules true Required for esbuild/swc compatibility
verbatimModuleSyntax true Enforces explicit type imports
forceConsistentCasingInFileNames true Path casing must match filesystem
esModuleInterop true CommonJS interop support
skipLibCheck true Skip checking .d.ts files
resolveJsonModule true Allow importing .json files
experimentalDecorators true Decorator support
emitDecoratorMetadata true Decorator metadata emission
sourceMap true Source maps for debugging
useDefineForClassFields true TC39 class field semantics

Key Implications#

strict: true enables all of these checks:

  • strictNullChecks -- null and undefined are distinct types
  • strictFunctionTypes -- Contravariant function parameter checking
  • strictBindCallApply -- Strict bind, call, apply typing
  • strictPropertyInitialization -- Class properties must be initialized
  • noImplicitAny -- No implicit any types
  • noImplicitThis -- No implicit this types
  • alwaysStrict -- Emit "use strict" in output

verbatimModuleSyntax: true requires explicit type annotations on imports:

ts
// Correct -- type import is explicit
import { type SomeType, someFunction } from './module.js';

// Correct -- all types
import type { SomeType, AnotherType } from './module.js';

// Incorrect -- will error because SomeType is only used as a type
import { SomeType, someFunction } from './module.js';

isolatedModules: true means each file is compiled independently. This prohibits:

  • const enum (use regular enum or union types instead)
  • Re-exporting types without type keyword
  • Namespace declarations that merge with values

Module System#

The monorepo uses ESM ("type": "module" in root package.json). All imports must use ESM syntax:

ts
// Correct
import { something } from './module.js';
export { something };

// Incorrect
const something = require('./module');
module.exports = something;

Path Aliases#

Path aliases are defined in tsconfig.base.json. The convention is:

  • Shared libraries: @oshun/{lib} maps to libs/shared/{lib}/src/index.ts
  • Domain libraries: @{domain}/{lib} maps to libs/{domain}/{lib}/src/index.ts

Examples:

ts
import { sql } from '@oshun/database';
import { AuthService } from '@oshun/auth';
import type { ConversationConfig } from '@iris/conversation-core';

ESLint Rules#

The root eslint.config.js uses the flat config format with typescript-eslint.

Global Rules (All TypeScript Files)#

Type Safety#

Rule Level Notes
@typescript-eslint/no-explicit-any warn Prefer typed alternatives. Use unknown for truly unknown types.
@typescript-eslint/no-unused-vars error Prefix with _ for intentionally unused: _unused, _err. Applies to args, vars, and caught errors.
@typescript-eslint/no-non-null-assertion warn Avoid value!. Use type narrowing or optional chaining instead.
@typescript-eslint/prefer-nullish-coalescing warn Use value ?? default instead of value || default.
@typescript-eslint/prefer-optional-chain warn Use obj?.prop?.method() instead of obj && obj.prop && obj.prop.method().
@typescript-eslint/explicit-function-return-type off Return types are inferred. Add them for public API clarity if desired.
@typescript-eslint/explicit-module-boundary-types off Module boundary return types are not required globally.

Best Practices#

Rule Level Notes
no-console warn Only console.warn and console.error are allowed. Use @oshun/logger for structured logging.
no-debugger error Must be removed before committing.
no-duplicate-imports error Combine value and type imports from the same module.
eqeqeq error Always use === and !==. Exception: null comparisons (== null allowed for checking both null and undefined).
prefer-const error Use const unless the variable is reassigned.
no-var error Always use let or const.

Context-Specific Relaxations#

Test files (*.spec.ts, *.test.ts, __tests__/**):

  • @typescript-eslint/no-explicit-any -- OFF
  • @typescript-eslint/no-non-null-assertion -- OFF
  • no-console -- OFF

Server/service files (**/server.ts, **/services/**):

  • no-console -- OFF

CLI applications (apps/**/cli/**):

  • no-console -- OFF

Config files (*.config.js, *.config.ts):

  • @typescript-eslint/no-require-imports -- OFF

Legacy code (lilith/**, yemaya/**):

  • @nx/enforce-module-boundaries -- OFF (disabled during migration)
  • @typescript-eslint/no-explicit-any -- warn (relaxed)
  • @typescript-eslint/no-require-imports -- OFF

Module Boundary Rules#

The @nx/enforce-module-boundaries rule enforces domain isolation. Every project must have scope, layer, and type tags in its project.json.

Scope tags define domain ownership:

text
scope:shared, scope:contracts, scope:auth,
scope:lilith, scope:yemaya, scope:isis, scope:iris,
scope:sophia, scope:hathor, scope:bellona, scope:aphrodite,
scope:nyx, scope:tara, scope:veritas, scope:psyche, ...

Layer tags define architectural layer:

text
layer:contracts -> layer:infra -> layer:data -> layer:domain -> layer:ui

Type tags define project kind:

text
type:app, type:lib, type:util, type:e2e

Constraints:

  • type:app can depend on type:lib and type:util
  • type:lib can depend on type:lib and type:util
  • type:e2e can depend on type:lib, type:util, and type:app

Prettier Formatting#

Prettier is the sole code formatter. It runs as part of the pre-commit hook via lint-staged and is checked in CI via pnpm format:check.

Configuration#

Prettier uses its default settings with the prettier-plugin-tailwindcss plugin for Tailwind CSS class sorting. No custom .prettierrc file exists at the root -- Prettier defaults apply.

Key defaults:

Setting Value
Print width 80
Tab width 2
Tabs No (spaces)
Semicolons Yes
Quotes Double
Trailing comma all
Bracket spacing Yes
Arrow parens always
End of line lf

File Types Formatted#

Prettier formats these file types (configured in lint-staged):

  • .js, .jsx, .ts, .tsx -- Also linted with ESLint first
  • .json
  • .md
  • .yaml, .yml

Running Prettier#

bash
# Format all files
pnpm format

# Check formatting (CI mode)
pnpm format:check

File Organization#

Library Structure#

Every library follows this standard structure:

text
libs/{domain}/{library-name}/
  src/
    index.ts          # Public API barrel file
    types.ts          # Type definitions
    {module}.ts       # Implementation files
    __tests__/        # Test directory (alternative to co-located tests)
      {module}.spec.ts
  project.json        # Nx project configuration
  package.json        # Package metadata and dependencies
  tsconfig.json       # Extends ../../tsconfig.base.json (or ../../../)
  tsconfig.lib.json   # Library-specific TS config
  tsconfig.spec.json  # Test-specific TS config
  vitest.config.ts    # Vitest configuration

Public API (index.ts)#

The index.ts barrel file defines the public API of the library. It should export all public types, functions, and classes:

ts
/**
 * @{scope}/{library-name}
 *
 * Description of what this library provides.
 *
 * @packageDocumentation
 */

// Types
export type { TypeA, TypeB, TypeC } from './types.js';

// Constants and defaults
export { DEFAULT_CONFIG, SOME_CONSTANT } from './types.js';

// Implementation
export { ServiceClass, createService } from './service.js';
export { helperFunction } from './helpers.js';

Conventions:

  • Group exports by category (types, constants, implementations)
  • Use section comments (// ====...) to visually separate categories
  • Include JSDoc module documentation at the top
  • Export types separately using export type (required by verbatimModuleSyntax)

Application Structure#

Applications follow a domain-appropriate structure. Common patterns:

text
apps/{domain}/{app-name}/
  src/
    app.ts            # Application entry point
    routes/           # API route handlers
    services/         # Business logic services
    middleware/       # Request middleware
  test/               # Test files
  project.json
  package.json
  tsconfig.json

Project Configuration (project.json)#

Every project must include scope, layer, and type tags:

json
{
  "name": "@domain/library-name",
  "sourceRoot": "libs/domain/library-name/src",
  "projectType": "library",
  "tags": ["scope:domain", "layer:domain", "type:lib"],
  "targets": {
    "build": {
      "executor": "@nx/js:tsc",
      "options": {
        "outputPath": "dist/libs/domain/library-name",
        "main": "libs/domain/library-name/src/index.ts",
        "tsConfig": "libs/domain/library-name/tsconfig.lib.json"
      }
    },
    "test": {
      "executor": "@nx/vite:test",
      "options": {
        "config": "libs/domain/library-name/vitest.config.ts"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint"
    }
  }
}

Naming Conventions#

Files and Directories#

Type Convention Example
Source files kebab-case.ts auth-service.ts
Test files *.spec.ts auth-service.spec.ts
Test files (alt) *.test.ts auth-service.test.ts
Type definition files types.ts types.ts
Index/barrel files index.ts index.ts
Config files *.config.ts vitest.config.ts
Directories kebab-case auth-primitives/
React components PascalCase.tsx MeditationCard.tsx
CSS modules *.module.css SafeArea.module.css

Code Identifiers#

Type Convention Example
Variables camelCase const userName = 'john';
Constants UPPER_CASE or camelCase const MAX_RETRIES = 3;
Functions camelCase function createService() {}
Classes PascalCase class AuthService {}
Interfaces PascalCase interface UserProfile {}
Type aliases PascalCase type TokenPair = { ... };
Enums PascalCase enum UserRole { Admin, User }
Enum members PascalCase UserRole.Admin
Generics PascalCase function wrap<T>(value: T): T {}
Private fields _camelCase private _connectionPool: Pool;
Unused params _camelCase (_req, res) => {}

Package Naming#

Type Pattern Example
Shared libraries @oshun/{lib-name} @oshun/database
Domain libraries @{domain}/{lib-name} @iris/conversation-core
Applications @{domain}/{app-name} @tara/api

Interface Naming#

Prefer descriptive names without I prefix for most interfaces. Use I prefix for repository/store interfaces that represent contracts:

ts
// Repository contracts -- use I prefix
interface IUserRepository { ... }
interface ITokenRepository { ... }
interface IAuditRepository { ... }

// Data shape interfaces -- no I prefix
interface UserProfile { ... }
interface AuthResult { ... }
interface TokenPair { ... }

Import Organization#

Import Order#

Organize imports in this order, separated by blank lines:

  1. Node.js built-in modules (node:fs, node:path, etc.)
  2. External packages (zod, hono, vitest, etc.)
  3. Workspace packages (@oshun/*, @iris/*, etc.)
  4. Relative imports (./module, ../module)
ts
import { readFile } from 'node:fs/promises';

import { z } from 'zod';
import { Hono } from 'hono';

import { sql, type ParameterizedQuery } from '@oshun/database';
import { AuthService } from '@oshun/auth';

import { processData } from './helpers.js';
import type { Config } from './types.js';

Import Rules#

  1. No duplicate imports (no-duplicate-imports: error). Combine value and type imports from the same module:

    ts
    // Correct
    import { sql, type ParameterizedQuery } from '@oshun/database';
    
    // Incorrect -- separate imports from same module
    import { sql } from '@oshun/database';
    import type { ParameterizedQuery } from '@oshun/database';
    
  2. Use type keyword for type-only imports (required by verbatimModuleSyntax):

    ts
    // Type-only import
    import type { UserProfile } from './types.js';
    
    // Mixed import with inline type
    import { AuthService, type AuthConfig } from '@oshun/auth';
    
  3. Use .js extension for relative imports (required for ESM):

    ts
    // Correct
    import { helper } from './helpers.js';
    
    // Incorrect
    import { helper } from './helpers';
    

Error Handling Patterns#

General Principles#

  1. Never swallow errors silently. Always log or re-throw.

  2. Use typed errors. The @oshun/errors library provides structured error types. Domain libraries define their own typed error classes:

    ts
    export class AuthError extends Error {
      constructor(
        public code: AuthErrorCode,
        message: string,
        public details?: Record<string, unknown>
      ) {
        super(message);
        this.name = 'AuthError';
      }
    }
    
  3. Use unknown for catch blocks (TypeScript strict mode requires this):

    ts
    try {
      await riskyOperation();
    } catch (error: unknown) {
      if (error instanceof AuthError) {
        // Handle typed error
      } else {
        // Handle unknown error
        throw new Error('Unexpected error', { cause: error });
      }
    }
    
  4. Prefix unused caught errors with _:

    ts
    try {
      await operation();
    } catch (_error) {
      // Intentionally ignoring the error
      return fallbackValue;
    }
    

Result Pattern#

For functions that can fail in expected ways, consider returning a result type instead of throwing:

ts
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function authenticate(
  credentials: LoginCredentials
): Promise<Result<AuthResult, AuthError>> {
  // ...
}

Never Expose Internal Errors#

API error responses must never include stack traces, internal paths, or implementation details in production:

ts
// Correct -- structured error response
return { error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } };

// Incorrect -- leaks internals
return { error: err.stack };

Testing Patterns#

Framework#

All tests use Vitest as the test runner, configured per-project via vitest.config.ts.

Vitest Configuration#

Each project's vitest.config.ts must include path aliases that match tsconfig.base.json:

ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
  },
  resolve: {
    alias: {
      '@oshun/database': '../database/src/index.ts',
      '@oshun/errors': '../errors/src/index.ts',
    },
  },
});

Test File Naming#

  • Unit tests: *.spec.ts or *.test.ts -- co-located with source or in __tests__/ directory
  • Integration tests: *.integration.test.ts or in test/ directory
  • E2E tests: In a dedicated e2e/ directory with Playwright

Test Structure#

Use descriptive describe and it blocks:

ts
import { describe, it, expect, beforeEach, vi } from 'vitest';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(() => {
    service = createAuthService(mockConfig);
  });

  describe('login', () => {
    it('returns token pair for valid credentials', async () => {
      const result = await service.login({
        email: 'user@example.com',
        password: 'ValidPass123!',
      });

      expect(result.success).toBe(true);
      expect(result.data.accessToken).toBeDefined();
      expect(result.data.refreshToken).toBeDefined();
    });

    it('returns error for invalid password', async () => {
      const result = await service.login({
        email: 'user@example.com',
        password: 'wrong',
      });

      expect(result.success).toBe(false);
      expect(result.error.code).toBe('INVALID_CREDENTIALS');
    });
  });
});

Assertion Patterns#

Common Vitest assertion patterns and their correct usage:

ts
// Equality
expect(value).toBe(expected); // Strict equality (===)
expect(value).toEqual(expected); // Deep equality
expect(value).toBeCloseTo(3.14, 2); // Floating point (within 0.005)

// Array/object matching
expect(array).toContain(item); // Uses === (for primitives)
expect(array).toContainEqual(item); // Uses deep equality (for objects)
expect(obj).toMatchObject(partial); // Partial object match

// Truthiness
expect(value).toBeDefined();
expect(value).toBeUndefined();
expect(value).toBeTruthy();
expect(value).toBeFalsy();

// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow('message');

Mocking#

Use Vitest's built-in mocking:

ts
import { vi } from 'vitest';

// Mock a module
vi.mock('@oshun/database', () => ({
  sql: vi.fn(),
  createPool: vi.fn(),
}));

// Mock a function
const mockFn = vi.fn().mockReturnValue('result');

// Spy on a method
const spy = vi.spyOn(service, 'method');

// Reset mocks between tests
afterEach(() => {
  vi.restoreAllMocks();
});

Domain-Specific Rules#

Uzume Domain (Live Performance & Stagecraft)#

The Uzume domain (libs/uzume/**) enforces stricter TypeScript rules due to its real-time nature and entertainment protocol integrations:

Rule Level Notes
@typescript-eslint/no-explicit-any error No any allowed
@typescript-eslint/explicit-function-return-type error All functions must have return types
@typescript-eslint/explicit-module-boundary-types error All exported functions must have return types

Naming conventions are strictly enforced:

  • Default identifiers: camelCase
  • Variables: camelCase, UPPER_CASE, or PascalCase
  • Types/interfaces: PascalCase
  • Enum members: UPPER_CASE or PascalCase
  • Object literal and type properties: unrestricted (for protocol compatibility)

Entertainment protocol abbreviations are allowed in identifiers: DMX, OSC, MIDI, SMPTE, GDTF, MVR, NDI, AES, LTC, MTC, RDM, ArtNet, sACN

Browser Extension (Iris)#

Browser extension files (apps/iris/web/extension/**/*.js) have explicit browser and WebExtension globals defined and relaxed rules for switch-case patterns.

PWA and Widget (Iris)#

PWA files (apps/iris/web/pwa/**) and widget files (apps/iris/web/widget/**) have no-console disabled and relaxed nullish coalescing rules for DOM manipulation patterns.

Code Generation Libraries (Iris)#

Code generation and agentic libraries (libs/iris/code/generation/**, libs/iris/code/agentic/**) have no-case-declarations disabled due to extensive switch-case patterns.


Pre-commit Enforcement Summary#

Every commit is automatically checked by these hooks (via Husky):

  1. Pre-commit (lint-staged):

    • eslint --fix on staged .js, .jsx, .ts, .tsx files
    • prettier --write on staged .js, .jsx, .ts, .tsx, .json, .md, .yaml, .yml files
  2. Commit-msg (commitlint):

    • Validates the commit message against Conventional Commits format
    • Enforces type, scope, subject, and length rules

If either hook fails, the commit is rejected. Fix the issues and try again.