Disciplines · Reference

Adding a New Library

Libraries in Oshun are reusable code packages that can be shared across applications and other libraries.

6sections2 minread

On this page

This guide walks through the process of adding a new shared library to the Oshun monorepo.

Overview#

Libraries in Oshun are reusable code packages that can be shared across applications and other libraries. They live in the libs/ directory and follow a structured organization pattern.

Library Categories#

Directory Purpose Tags
libs/shared/ Cross-domain utilities scope:shared
libs/contracts/ API contracts, events, types scope:shared, layer:contracts
libs/{domain}/ Domain-specific libraries scope:{domain}

Domains with apps and libraries: iris, lilith, yemaya, isis, sophia, hathor, bellona, tara, veritas, psyche, nyx, aja, aphrodite.

Library-only domains: aje, themis, galatea, shakti, nous, uzume, demeter, euterpe, hestia, arete, kuanyin, meditation.

Step-by-Step Guide#

Step 1: Plan Your Library#

Before creating a library, consider:

  1. Purpose: What problem does this library solve?
  2. Scope: Which domains will use it?
  3. Dependencies: What does it depend on?
  4. Consumers: Who will import this library?

Step 2: Choose the Right Location#

text
libs/
├── shared/              # Cross-domain utilities (any domain can use)
│   ├── utils/           # Common utilities
│   ├── types/           # Shared TypeScript types
│   └── {your-lib}/      # ← Add here for shared utilities
│
├── contracts/           # Cross-domain contracts
│   ├── events/          # Event schemas
│   └── types/           # Shared types
│
└── {domain}/            # Domain-specific libraries
    ├── database/        # Domain database client
    ├── client/          # Domain API client
    └── {your-lib}/      # ← Add here for domain-specific code

Step 3: Create the Library#

bash
# Shared utility library
nx g @nx/js:library my-lib --directory=libs/shared/my-lib --importPath=@oshun/my-lib

# Domain-specific library
nx g @nx/js:library my-lib --directory=libs/isis/my-lib --importPath=@isis/my-lib

Manual Creation#

Create the directory structure:

text
libs/shared/my-lib/
├── src/
│   ├── index.ts           # Public API exports
│   ├── my-lib.ts          # Main implementation
│   └── my-lib.spec.ts     # Tests
├── project.json           # Nx project configuration
├── package.json           # Package metadata
├── tsconfig.json          # TypeScript configuration
├── tsconfig.lib.json      # Build configuration
├── tsconfig.spec.json     # Test configuration
└── README.md              # Documentation

Step 4: Configure project.json#

Create libs/shared/my-lib/project.json:

json
{
  "name": "@oshun/my-lib",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "libs/shared/my-lib/src",
  "projectType": "library",
  "tags": ["scope:shared", "type:lib", "layer:domain"],
  "targets": {
    "build": {
      "executor": "@nx/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/libs/shared/my-lib",
        "main": "libs/shared/my-lib/src/index.ts",
        "tsConfig": "libs/shared/my-lib/tsconfig.lib.json",
        "assets": ["libs/shared/my-lib/*.md"]
      }
    },
    "test": {
      "executor": "@nx/vite:test",
      "options": {
        "config": "libs/shared/my-lib/vitest.config.ts"
      }
    },
    "lint": {
      "executor": "@nx/eslint:lint",
      "outputs": ["{options.outputFile}"]
    },
    "typecheck": {
      "executor": "nx:run-commands",
      "options": {
        "cwd": "libs/shared/my-lib",
        "command": "tsc --noEmit"
      }
    }
  }
}

Step 5: Configure package.json#

Create libs/shared/my-lib/package.json:

json
{
  "name": "@oshun/my-lib",
  "version": "0.1.0",
  "description": "Description of your library",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist", "README.md"],
  "scripts": {
    "build": "tsc -p tsconfig.lib.json",
    "test": "vitest run",
    "lint": "eslint src --ext ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    // Add your dependencies here
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vitest": "catalog:"
  },
  "peerDependencies": {
    // Add peer dependencies if needed
  }
}

Step 6: Configure TypeScript#

Create libs/shared/my-lib/tsconfig.json:

json
{
  "extends": "../../../tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "declarationMap": true,
    "types": ["vitest/globals"]
  },
  "files": [],
  "include": [],
  "references": [
    { "path": "./tsconfig.lib.json" },
    { "path": "./tsconfig.spec.json" }
  ]
}

Create libs/shared/my-lib/tsconfig.lib.json:

json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "rootDir": "./src"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
}

Create libs/shared/my-lib/tsconfig.spec.json:

json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/test",
    "types": ["vitest/globals"]
  },
  "include": ["src/**/*.spec.ts", "src/**/*.test.ts"]
}

Step 7: Configure Vitest#

Create libs/shared/my-lib/vitest.config.ts:

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

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.{test,spec}.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Step 8: Implement Your Library#

Create libs/shared/my-lib/src/index.ts:

typescript
/**
 * @oshun/my-lib - Description of your library
 *
 * @packageDocumentation
 */

export { MyClass, MyFunction, MyType } from './my-lib';

Create libs/shared/my-lib/src/my-lib.ts:

typescript
/**
 * Example interface
 */
export interface MyType {
  id: string;
  name: string;
  value: number;
}

/**
 * Example class
 */
export class MyClass {
  private data: MyType[];

  constructor() {
    this.data = [];
  }

  add(item: MyType): void {
    this.data.push(item);
  }

  get(id: string): MyType | undefined {
    return this.data.find((item) => item.id === id);
  }

  getAll(): MyType[] {
    return [...this.data];
  }
}

/**
 * Example function
 *
 * @param input - The input value
 * @returns The processed output
 */
export function MyFunction(input: string): string {
  return input.toUpperCase();
}

Step 9: Write Tests#

Create libs/shared/my-lib/src/my-lib.spec.ts:

typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { MyClass, MyFunction, MyType } from './my-lib';

describe('MyClass', () => {
  let instance: MyClass;

  beforeEach(() => {
    instance = new MyClass();
  });

  describe('add', () => {
    it('should add items', () => {
      const item: MyType = { id: '1', name: 'Test', value: 42 };
      instance.add(item);
      expect(instance.getAll()).toHaveLength(1);
    });
  });

  describe('get', () => {
    it('should retrieve items by id', () => {
      const item: MyType = { id: '1', name: 'Test', value: 42 };
      instance.add(item);
      expect(instance.get('1')).toEqual(item);
    });

    it('should return undefined for non-existent ids', () => {
      expect(instance.get('non-existent')).toBeUndefined();
    });
  });
});

describe('MyFunction', () => {
  it('should convert input to uppercase', () => {
    expect(MyFunction('hello')).toBe('HELLO');
  });

  it('should handle empty strings', () => {
    expect(MyFunction('')).toBe('');
  });
});

Step 10: Add to Path Mappings#

Update tsconfig.base.json at the repository root:

json
{
  "compilerOptions": {
    "paths": {
      "@oshun/my-lib": ["libs/shared/my-lib/src/index.ts"]
      // ... other paths
    }
  }
}

Step 11: Create README#

Create libs/shared/my-lib/README.md:

markdown
# @oshun/my-lib

Description of your library.

## Installation

This library is part of the Oshun monorepo and is available as a workspace
dependency.

\`\`\`bash pnpm add @oshun/my-lib \`\`\`

## Usage

\`\`\`typescript import { MyClass, MyFunction } from '@oshun/my-lib';

const instance = new MyClass(); instance.add({ id: '1', name: 'Example', value:
100 });

const result = MyFunction('hello'); \`\`\`

## API

### MyClass

- `add(item: MyType): void` - Add an item
- `get(id: string): MyType | undefined` - Get item by ID
- `getAll(): MyType[]` - Get all items

### MyFunction

- `MyFunction(input: string): string` - Convert input to uppercase

Step 12: Verify the Library#

bash
# Build the library
nx build @oshun/my-lib

# Run tests
nx test @oshun/my-lib

# Run linting
nx lint @oshun/my-lib

# Type check
nx typecheck @oshun/my-lib

# View in dependency graph
nx graph

Best Practices#

1. Single Responsibility#

Each library should have a clear, focused purpose:

text
✅ @oshun/retry      # Just retry logic
✅ @oshun/cache      # Just caching
✅ @oshun/validation # Just validation

❌ @oshun/utils      # Too broad - split into specific utilities

2. Explicit Exports#

Only export what consumers need:

typescript
// index.ts

// ✅ Explicit exports
export { PublicClass } from './public-class';
export type { PublicType } from './types';

// ❌ Don't re-export internal utilities
// export * from './internal-helpers';

3. Proper Dependency Management#

Distinguish between dependency types:

json
{
  "dependencies": {
    // Runtime dependencies required by consumers
    "zod": "catalog:"
  },
  "devDependencies": {
    // Only needed during development/testing
    "vitest": "catalog:"
  },
  "peerDependencies": {
    // Must be provided by consumer
    "typescript": "catalog:"
  }
}

4. Use Appropriate Tags#

Apply the correct tags for module boundaries:

Situation Tags
Shared utility for all domains scope:shared, type:lib
Domain-specific business logic scope:{domain}, type:lib, layer:domain
Database access layer scope:{domain}, type:lib, layer:data
API contracts scope:shared, type:lib, layer:contracts

5. Write Comprehensive Tests#

Aim for high test coverage:

typescript
// Test public API thoroughly
describe('PublicFunction', () => {
  // Happy path
  it('should handle valid input', () => {});

  // Edge cases
  it('should handle empty input', () => {});
  it('should handle null/undefined', () => {});

  // Error cases
  it('should throw on invalid input', () => {});
});

6. Document Your API#

Use JSDoc comments for documentation:

typescript
/**
 * Performs an operation with retry logic.
 *
 * @param operation - The async operation to retry
 * @param options - Retry configuration options
 * @returns The result of the operation
 * @throws {RetryError} When all retry attempts fail
 *
 * @example
 * ```typescript
 * const result = await retry(
 *   () => fetchData(),
 *   { maxRetries: 3, delay: 1000 }
 * );
 * ```
 */
export async function retry<T>(
  operation: () => Promise<T>,
  options: RetryOptions
): Promise<T> {
  // ...
}

Troubleshooting#

Library not found when importing#

  1. Check path mappings in tsconfig.base.json
  2. Run nx reset to clear cache
  3. Restart your IDE's TypeScript server

Circular dependency detected#

  1. Use nx graph to visualize dependencies
  2. Extract shared code to a new library
  3. Use dependency injection to break cycles

Build fails with type errors#

  1. Ensure tsconfig.lib.json excludes test files
  2. Check that all types are properly exported
  3. Verify peer dependencies are installed

Checklist#

Before submitting your PR:

  • Library compiles without errors
  • All tests pass
  • Linting passes
  • TypeScript types are correct
  • README is complete
  • Public API is documented
  • Added to path mappings in tsconfig.base.json
  • Tags are set correctly in project.json