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:
- Purpose: What problem does this library solve?
- Scope: Which domains will use it?
- Dependencies: What does it depend on?
- Consumers: Who will import this library?
Step 2: Choose the Right Location#
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#
Using Nx Generator (Recommended)#
# 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:
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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
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:
/**
* @oshun/my-lib - Description of your library
*
* @packageDocumentation
*/
export { MyClass, MyFunction, MyType } from './my-lib';
Create libs/shared/my-lib/src/my-lib.ts:
/**
* 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:
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:
{
"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:
# @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#
# 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:
✅ @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:
// 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:
{
"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:
// 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:
/**
* 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#
- Check path mappings in
tsconfig.base.json - Run
nx resetto clear cache - Restart your IDE's TypeScript server
Circular dependency detected#
- Use
nx graphto visualize dependencies - Extract shared code to a new library
- Use dependency injection to break cycles
Build fails with type errors#
- Ensure
tsconfig.lib.jsonexcludes test files - Check that all types are properly exported
- 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