Module Exports Patterns
🚨 CRITICAL RULES
1. Factory Function is Primary Export
Every module's src/index.ts MUST export a factory function:
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
Rules:
- •✅ Named export (NOT default export)
- •✅ Function name starts with
new - •✅ Returns Impl instance (which implements Connector)
- •✅ No parameters
- •❌ NEVER use default export
2. Re-export Generated Types
MUST re-export all generated types:
// Re-export all generated API interfaces and wrappers export * from '../generated/api'; // Re-export all generated models export * from '../generated/model';
Why:
- •Consumers need access to types
- •ConnectionProfile, ConnectionState, etc.
- •Resource models (User, Group, etc.)
- •Don't pick specific types - export all
3. Minimal Public API
ONLY export what consumers actually need:
Export:
- •✅ Factory function
- •✅ Generated types (api.ts, model.ts)
- •✅ Custom profile types (if any)
- •✅ Custom error types (if any)
DO NOT export:
- •❌ Client class (internal implementation)
- •❌ Producer implementation classes (internal)
- •❌ Mapper functions (internal)
- •❌ Helper/utility functions (internal)
- •❌ HTTP interceptors (internal)
🟡 STANDARD RULES
Standard index.ts Template
Basic pattern:
// src/index.ts
// Import Impl class
import { ServiceImpl } from './ServiceImpl';
// ========================================
// Factory Function (Primary Export)
// ========================================
/**
* Creates a new Service connector instance
* @returns ServiceImpl instance that implements ServiceConnector
*/
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
// ========================================
// Generated Exports
// ========================================
// Re-export all generated API interfaces and wrappers
export * from '../generated/api';
// Re-export all generated models
export * from '../generated/model';
// ========================================
// Custom Type Exports (if needed)
// ========================================
// Export custom profile types if you have them
// export type { CustomProfile } from './ServiceClient';
// Export custom error types if you have them
// export type { CustomError } from './errors';
Factory Function Naming
Pattern: new + PascalCase service name
Examples:
// GitHub module export function newGitHub(): GitHubImpl // Avigilon Alta Access module export function newAvigilonAltaAccess(): AccessImpl // Slack module export function newSlack(): SlackImpl // Azure Active Directory module export function newAzureActiveDirectory(): AzureActiveDirectoryImpl
Rules:
- •✅ Always starts with
new - •✅ Service name in PascalCase
- •✅ Full service name (not abbreviated)
- •✅ Returns Impl class
- •❌ No abbreviations (newAAD ❌, newAzureActiveDirectory ✅)
What Gets Re-exported from generated/
From ../generated/api.ts:
export * from '../generated/api';
This exports:
- •
ServiceConnectorinterface - •
<Resource>Apiinterfaces (UserApi, GroupApi, etc.) - •
wrap<Resource>Producerfunctions - •Type definitions for all operations
From ../generated/model.ts:
export * from '../generated/model';
This exports:
- •
ConnectionProfiletype - •
ConnectionStatetype - •Resource types (User, Group, Acu, etc.)
- •Request/Response types
- •Enum types
Optional Exports
Export Impl class (optional):
// Export implementation class for advanced usage
export { ServiceImpl } from './ServiceImpl';
When to export:
- •✅ If consumers might need to extend it
- •✅ If consumers need type for instance checks
- •⚠️ Usually not necessary - factory function is enough
Export Client class (rarely):
// Export client for advanced/testing usage
export { ServiceClient } from './ServiceClient';
When to export:
- •⚠️ Rarely needed
- •⚠️ Only if consumers need direct access for testing
- •⚠️ Generally keep Client internal
Export mappers (for testing):
// Export mappers for testing/debugging export * from './Mappers';
When to export:
- •✅ If consumers might need to test mappings
- •✅ If mappers are useful utilities
- •⚠️ Consider if mappers should be internal
Custom Type Exports
If you have custom ConnectionProfile extensions:
// ServiceClient.ts
export interface ServiceProfile extends ConnectionProfile {
region?: string;
customSetting?: boolean;
}
// index.ts
export type { ServiceProfile } from './ServiceClient';
If you have custom error types:
// errors.ts
export class ServiceSpecificError extends Error {
constructor(message: string) {
super(message);
this.name = 'ServiceSpecificError';
}
}
// index.ts
export { ServiceSpecificError } from './errors';
Export Organization
Group exports by purpose with comments:
// src/index.ts
import { ServiceImpl } from './ServiceImpl';
// ========================================
// Factory Function
// ========================================
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
// ========================================
// Implementation Classes (for advanced usage)
// ========================================
export { ServiceImpl } from './ServiceImpl';
// export { ServiceClient } from './ServiceClient'; // Uncomment if needed
// ========================================
// Generated Types
// ========================================
export * from '../generated/api';
export * from '../generated/model';
// ========================================
// Mappers (for testing)
// ========================================
export * from './Mappers';
// ========================================
// Custom Types
// ========================================
// export type { ServiceProfile } from './ServiceClient';
// export { ServiceSpecificError } from './errors';
🟢 GUIDELINES
Type Safety in Exports
Factory function should have return type:
// ✅ GOOD: Explicit return type
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
// ⚠️ OK but less clear
export function newServiceName() {
return new ServiceImpl();
}
Why:
- •Explicit type helps consumers
- •IDE autocomplete works better
- •Catches return type errors
Default Exports (Don't Use)
❌ WRONG: Default export
export default function newServiceName() {
return new ServiceImpl();
}
✅ CORRECT: Named export
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
Why:
- •Named exports are more explicit
- •Better for tree-shaking
- •Easier to find usages
- •Consistent with TypeScript best practices
Consumer Usage Pattern
How consumers use your module:
// Consumer code
import {
newServiceName,
ConnectionProfile,
UserApi,
User
} from '@auditmation/connector-vendor-service';
// Create connector using factory
const connector = newServiceName();
// Use ConnectionProfile type
const profile: ConnectionProfile = {
apiKey: process.env.API_KEY!
};
// Connect
await connector.connect(profile);
// Get typed API
const userApi: UserApi = connector.getUserApi();
// Get typed resources
const users: User[] = await userApi.listUsers();
What they need:
- •Factory function -
newServiceName() - •Profile type -
ConnectionProfile - •API interfaces -
UserApi - •Resource types -
User
All of these come from your exports!
Documentation Comments
Add JSDoc to factory function:
/**
* Creates a new Service connector instance.
*
* @returns A new connector instance that implements ServiceConnector
*
* @example
* ```typescript
* import { newServiceName } from '@auditmation/connector-vendor-service';
*
* const connector = newServiceName();
* await connector.connect({ apiKey: 'your-key' });
* ```
*/
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
Benefits:
- •Shows up in IDE tooltips
- •Documents usage
- •Provides example
Impl Class Export Pattern
If exporting Impl, also export it directly:
// Option 1: Export from source
export { ServiceImpl } from './ServiceImpl';
// Option 2: Re-export from factory
import { ServiceImpl } from './ServiceImpl';
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
export { ServiceImpl }; // Also export class
Enables type checking:
// Consumer code
import { newServiceName, ServiceImpl } from '@auditmation/connector';
const connector = newServiceName();
if (connector instanceof ServiceImpl) {
// TypeScript knows the type
}
Validation
Check Factory Function Exists
# Check factory function exists
if [ -f src/index.ts ]; then
if grep -q "export function new" src/index.ts; then
echo "✅ PASS: Factory function exists"
else
echo "❌ FAIL: No factory function found"
exit 1
fi
else
echo "❌ FAIL: src/index.ts not found"
exit 1
fi
Check Factory Function Naming
# Check factory function follows naming convention FACTORY_NAME=$(grep "export function new" src/index.ts | sed -E 's/.*function (new[A-Za-z]+).*/\1/') if [[ "$FACTORY_NAME" =~ ^new[A-Z] ]]; then echo "✅ PASS: Factory function named: $FACTORY_NAME" else echo "❌ FAIL: Factory function must start with 'new' + PascalCase" exit 1 fi
Check Generated Exports
# Check re-exports from generated/ if grep -q "export \* from.*generated" src/index.ts; then echo "✅ PASS: Re-exports generated types" else echo "❌ FAIL: Must re-export from ../generated/" exit 1 fi # Check both api and model exports if grep -q "export \* from.*generated/api" src/index.ts; then echo "✅ PASS: Exports generated/api" else echo "⚠️ WARN: Should export from ../generated/api" fi if grep -q "export \* from.*generated/model" src/index.ts; then echo "✅ PASS: Exports generated/model" else echo "⚠️ WARN: Should export from ../generated/model" fi
Check No Internal Exports
# Check NOT exporting Client class (usually internal) if grep -q "export.*Client.*from.*Client" src/index.ts | grep -v "//"; then echo "⚠️ WARN: Exporting Client class (usually should be internal)" else echo "✅ PASS: Client class not exported (good - keep internal)" fi # Check for default exports (should not use) if grep -q "export default" src/index.ts; then echo "❌ FAIL: Using default export (use named exports)" exit 1 else echo "✅ PASS: No default exports (good)" fi
Check Return Type
# Check factory function has return type if grep -q "export function new.*().*:" src/index.ts; then echo "✅ PASS: Factory function has return type" else echo "⚠️ WARN: Factory function should have explicit return type" fi
Complete Validation Script
#!/bin/bash
# validate-exports.sh - Validate index.ts exports
echo "=== Module Exports Validation ==="
echo ""
if [ ! -f src/index.ts ]; then
echo "❌ FAIL: src/index.ts not found"
exit 1
fi
echo "Checking: src/index.ts"
echo ""
ERRORS=0
# 1. Factory function
echo "1. Factory Function:"
if grep -q "export function new" src/index.ts; then
FACTORY=$(grep "export function new" src/index.ts | head -1)
echo " ✅ Found: $FACTORY"
# Check naming
if echo "$FACTORY" | grep -q "export function new[A-Z]"; then
echo " ✅ Correct naming (new + PascalCase)"
else
echo " ❌ Wrong naming (must be new + PascalCase)"
ERRORS=$((ERRORS + 1))
fi
# Check return type
if echo "$FACTORY" | grep -q "):"; then
echo " ✅ Has return type"
else
echo " ⚠️ Should have explicit return type"
fi
else
echo " ❌ Factory function missing"
ERRORS=$((ERRORS + 1))
fi
echo ""
# 2. Generated exports
echo "2. Generated Exports:"
if grep -q "export \* from.*generated" src/index.ts; then
echo " ✅ Re-exports from generated/"
if grep -q "export \* from.*generated/api" src/index.ts; then
echo " ✅ Exports ../generated/api"
else
echo " ⚠️ Should export ../generated/api"
fi
if grep -q "export \* from.*generated/model" src/index.ts; then
echo " ✅ Exports ../generated/model"
else
echo " ⚠️ Should export ../generated/model"
fi
else
echo " ❌ Must re-export from generated/"
ERRORS=$((ERRORS + 1))
fi
echo ""
# 3. Export style
echo "3. Export Style:"
if grep -q "export default" src/index.ts; then
echo " ❌ Uses default export (should use named exports)"
ERRORS=$((ERRORS + 1))
else
echo " ✅ No default exports (good)"
fi
# Count named exports
EXPORT_COUNT=$(grep -c "^export" src/index.ts)
echo " ✅ $EXPORT_COUNT export statements"
echo ""
# 4. Internal exports check
echo "4. Internal Exports (warnings):"
if grep -q "export.*Client" src/index.ts | grep -v "^//"; then
echo " ⚠️ Exporting Client (usually internal)"
else
echo " ✅ Client not exported"
fi
if grep -q "export.*Mapper" src/index.ts | grep -v "^//"; then
echo " ⚠️ Exporting Mappers (check if needed)"
else
echo " ✅ Mappers not exported"
fi
echo ""
# 5. File size check (should be small)
LINE_COUNT=$(wc -l < src/index.ts)
if [ "$LINE_COUNT" -lt 50 ]; then
echo "5. File Size: ✅ $LINE_COUNT lines (good - index.ts should be small)"
else
echo "5. File Size: ⚠️ $LINE_COUNT lines (large - consider if all exports are needed)"
fi
echo ""
# Summary
if [ $ERRORS -eq 0 ]; then
echo "=== ✅ VALIDATION PASSED ==="
exit 0
else
echo "=== ❌ VALIDATION FAILED ($ERRORS errors) ==="
exit 1
fi
Common Issues
Issue: No factory function
Problem:
// index.ts
export { ServiceImpl } from './ServiceImpl';
Solution:
import { ServiceImpl } from './ServiceImpl';
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
export * from '../generated/api';
export * from '../generated/model';
Issue: Using default export
Problem:
export default function newServiceName() {
return new ServiceImpl();
}
Solution:
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
Issue: Not re-exporting generated types
Problem:
// Only exports factory
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
Solution:
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
// Add these
export * from '../generated/api';
export * from '../generated/model';
Issue: Exporting too much
Problem:
// Exporting everything export * from './ServiceImpl'; export * from './ServiceClient'; export * from './helpers'; export * from './utils'; export * from './mappers'; export * from './errors';
Solution:
// Minimal public API
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
export * from '../generated/api';
export * from '../generated/model';
// Only export what consumers truly need
Anti-Patterns
❌ BAD: Complex factory with parameters
export function newServiceName(config: ServiceConfig): ServiceImpl {
return new ServiceImpl(config);
}
✅ GOOD: Simple factory, no parameters
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
❌ BAD: Multiple factory functions
export function newServiceName(): ServiceImpl { ... }
export function createServiceName(): ServiceImpl { ... }
export function makeServiceName(): ServiceImpl { ... }
✅ GOOD: Single factory function
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
❌ BAD: Selective type exports
// Don't cherry-pick types
export { ConnectionProfile, UserApi } from '../generated/api';
export { User, Group } from '../generated/model';
✅ GOOD: Export all generated types
// Export everything export * from '../generated/api'; export * from '../generated/model';
❌ BAD: Exporting implementation details
export { ServiceClient } from './ServiceClient';
export { httpInterceptor } from './interceptors';
export { mapUser, mapGroup } from './mappers';
export { validateProfile } from './validators';
✅ GOOD: Minimal public API
export function newServiceName(): ServiceImpl {
return new ServiceImpl();
}
export * from '../generated/api';
export * from '../generated/model';
// Keep internals private