A tiny NestJS-native library for building pluggable adapters (Ports & Adapters / Hexagonal) using class-based Dynamic Modules, with great DX and strong type safety.
npm install nest-hex
![]()





> A tiny, class-based, NestJS-native library for building pluggable adapters following the Ports & Adapters (Hexagonal Architecture) pattern with minimal boilerplate.
- What is nest-hex?
- Why Hexagonal Architecture?
- Features
- Installation
- Quick Start
- CLI
- Key Benefits
- Swappable Infrastructure
- Advanced Features
- Full Documentation
- Examples
nest-hex eliminates boilerplate when building NestJS applications with the Ports & Adapters (Hexagonal Architecture) pattern. It provides decorators and base classes that handle all the repetitive wiring, letting you focus on business logic.
- ๐งช Testable - Mock infrastructure easily, test business logic in isolation
- ๐ Swappable - Switch from S3 to Azure Blob Storage without touching domain code
- ๐ฏ Clean - Keep business logic free of infrastructure concerns
- ๐ Flexible - Use different adapters for dev, test, and production
- ๐ฏ Declarative - Declare port tokens and implementations once using @Adapter({ portToken, implementation })
- ๐๏ธ Class-based - Standard NestJS dynamic modules, no function factories
- ๐ Type-safe - Compile-time proof that adapters provide the correct port tokens
- โก Zero runtime overhead - Uses TypeScript decorators and metadata
- ๐ฆ Tiny - Core library under 1KB minified
- ๐ ๏ธ Powerful CLI - Generate ports, adapters, and services instantly
``bash`
npm install nest-hexor
yarn add nest-hexor
pnpm add nest-hexor
bun add nest-hex
`typescript
// storage.port.ts
export const STORAGE_PORT = Symbol('STORAGE_PORT')
export interface StoragePort {
upload(key: string, data: Buffer): Promise
download(key: string): Promise
}
`
`typescript
// s3.types.ts - Configuration types
import type { AdapterConfig } from 'nest-hex'
import type { STORAGE_PORT, StoragePort } from './storage.port'
export interface S3ConfigOptions {
bucket: string
region: string
}
export type StorageToken = typeof STORAGE_PORT
export type S3AdapterConfig = AdapterConfig
// s3.service.ts - Implementation service
import { Injectable } from '@nestjs/common'
import type { StoragePort } from './storage.port'
import type { S3ConfigOptions } from './s3.types'
@Injectable()
export class S3Service implements StoragePort {
constructor(private options: S3ConfigOptions) {}
async upload(key: string, data: Buffer): Promise
// AWS S3 upload logic here
return https://s3.amazonaws.com/${this.options.bucket}/${key}
}
async download(key: string): Promise
// AWS S3 download logic here
return Buffer.from('file contents')
}
}
// s3.adapter.ts - Adapter module
import { Adapter, AdapterBase } from 'nest-hex'
import { STORAGE_PORT } from './storage.port'
import { S3Service } from './s3.service'
import type { S3AdapterConfig, S3ConfigOptions } from './s3.types'
// Single decorator with type safety!
@Adapter
portToken: STORAGE_PORT,
implementation: S3Service
})
export class S3Adapter extends AdapterBase
`
`typescript
// file.service.ts
import { Injectable } from '@nestjs/common'
import { InjectPort } from 'nest-hex'
import { STORAGE_PORT, type StoragePort } from './storage.port'
@Injectable()
export class FileService {
constructor(
@InjectPort(STORAGE_PORT)
private readonly storage: StoragePort
) {}
async uploadUserAvatar(userId: string, image: Buffer): Promise
const key = avatars/${userId}.jpg`
return this.storage.upload(key, image)
}
}
`typescript
// file.module.ts
import { Module } from '@nestjs/common'
import { DomainModule } from 'nest-hex'
import { FileService } from './file.service'
@Module({})
export class FileModule extends DomainModule {}
`
`typescript
// app.module.ts
import { Module } from '@nestjs/common'
import { FileModule } from './file.module'
import { S3Adapter } from './s3.adapter'
@Module({
imports: [
FileModule.register({
adapter: S3Adapter.register({
bucket: process.env.S3_BUCKET || 'my-bucket',
region: process.env.AWS_REGION || 'us-east-1'
})
})
]
})
export class AppModule {}
`
That's it! You now have a fully type-safe, pluggable storage adapter. ๐
Generate ports, adapters, and services instantly with the built-in CLI:
`bashInitialize configuration
npx nest-hex init
See CLI Documentation for complete command reference, configuration options, and template customization.
Key Benefits
$3
`typescript
@Module({})
export class S3StorageModule {
static register(options: S3Options): DynamicModule {
return {
module: S3StorageModule,
providers: [
S3StorageService,
{ provide: STORAGE_PORT, useExisting: S3StorageService },
// More boilerplate...
],
exports: [STORAGE_PORT],
}
}
}
`$3
`typescript
// s3.types.ts
export type StorageToken = typeof STORAGE_PORT
export type S3AdapterConfig = AdapterConfig// s3.adapter.ts
@Adapter({
portToken: STORAGE_PORT,
implementation: S3StorageService
})
export class S3Adapter extends AdapterBase {}
`Swappable Infrastructure
The real power: swap infrastructure without touching business logic.
`typescript
// Development: Use local filesystem
const adapter = process.env.NODE_ENV === 'production'
? S3Adapter.register({ bucket: 'prod-bucket', region: 'us-east-1' })
: LocalStorageAdapter.register({ basePath: './uploads' })@Module({
imports: [FileModule.register({ adapter })]
})
export class AppModule {}
`Your
FileService business logic never changes. Only the adapter changes.Advanced Features
$3
`typescript
@Module({
imports: [
FileModule.register({
adapter: S3Adapter.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
bucket: config.get('S3_BUCKET')!,
region: config.get('AWS_REGION')!
})
})
})
]
})
export class AppModule {}
`$3
`typescript
// axios.types.ts
export type HttpClientToken = typeof HTTP_CLIENT_PORT
export type AxiosAdapterConfig = AdapterConfig// axios.adapter.ts
@Adapter({
portToken: HTTP_CLIENT_PORT,
implementation: AxiosHttpClient,
imports: [HttpModule],
providers: [
{ provide: 'HTTP_CONFIG', useValue: { timeout: 5000 } }
]
})
export class AxiosAdapter extends AdapterBase {}
`$3
`typescript
// mock-storage.types.ts
export type StorageToken = typeof STORAGE_PORT
export type MockStorageAdapterConfig = AdapterConfig// mock-storage.service.ts
@Injectable()
class MockStorageService implements StoragePort {
async upload(key: string, data: Buffer): Promise {
return
mock://storage/${key}
}
async download(key: string): Promise {
return Buffer.from('mock data')
}
}// mock-storage.adapter.ts
@Adapter({
portToken: STORAGE_PORT,
implementation: MockStorageService
})
export class MockStorageAdapter extends AdapterBase<{}> {}
// Use in tests
const module = await Test.createTestingModule({
imports: [
FileModule.register({
adapter: MockStorageAdapter.register({})
})
]
}).compile()
`Documentation
๐ Complete Documentation:
- Library Documentation - Full API reference, architecture guide, advanced patterns, and examples
- CLI Documentation - Complete CLI reference, configuration, templates, and best practices
๐ Quick Links:
- Core Concepts - Understand ports, adapters, and services
- Why Hexagonal Architecture? - Benefits with code examples
- Architecture Overview - Visual diagrams
- API Reference - Complete API documentation
- Testing Guide - Mock adapters and integration testing
- Migration Guide - Upgrading from @Port to @Adapter
Examples
examples/` directory for complete working examples:- Object Storage - S3 adapter with file upload/download
- Currency Rates - HTTP API adapter with rate conversion
- Mock Patterns - Testing with mock adapters
MIT ยฉ [Your Name]
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and development process.
---
Built with โค๏ธ for the NestJS community