@azversan/storage
v1.1.0
Published
A flexible NestJS storage module with support for Local, GCS, and S3 drivers
Readme
@azversan/storage
A flexible, production-ready NestJS storage module with built-in support for Local, Google Cloud Storage (GCS), and AWS S3 (and S3-compatible) drivers.
- @azversan/storage
Features
- 🗂️ Three built-in drivers — Local filesystem, AWS S3 (+ MinIO / Cloudflare R2), Google Cloud Storage
- ⚡ Dynamic NestJS module — synchronous and asynchronous registration patterns
- 🔢 Batch registration —
registerMany()/registerManyAsync()to wire multiple storages in one call - 🔑 Injection decorator —
@InjectStorage('name')for clean constructor injection - 🗃️ Registry service —
StorageRegistryto resolve any driver by name at runtime - 🔒 Signed URLs — temporary pre-signed access URLs for private objects (S3 & GCS)
- 🧩 Extensible — extend
StorageDriverto build your own backend
Installation
npm install @azversan/storageRequired peer dependencies:
npm install @nestjs/common @nestjs/core reflect-metadata rxjsOptional peer dependencies — install only the drivers you need:
# AWS S3 / MinIO / Cloudflare R2
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
# Google Cloud Storage
npm install @google-cloud/storageQuick Start
Local Driver
import { Module } from '@nestjs/common';
import { StorageModule, LocalStorageDriver } from '@azversan/storage';
@Module({
imports: [
StorageModule.register({
name: 'avatars',
driverClass: LocalStorageDriver,
options: {
dest: './uploads/avatars',
baseUrl: 'http://localhost:3000/uploads/avatars',
},
}),
],
})
export class AppModule {}AWS S3 Driver
import { Module } from '@nestjs/common';
import { StorageModule, S3StorageDriver } from '@azversan/storage';
@Module({
imports: [
StorageModule.register({
name: 'documents',
driverClass: S3StorageDriver,
options: {
region: 'us-east-1',
bucket: 'my-documents-bucket',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
}),
],
})
export class AppModule {}MinIO / S3-compatible:
StorageModule.register({
name: 'minio',
driverClass: S3StorageDriver,
options: {
region: 'us-east-1',
bucket: 'my-bucket',
endpoint: 'http://localhost:9000',
forcePathStyle: true,
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
},
});Google Cloud Storage Driver
import { Module } from '@nestjs/common';
import { StorageModule, GcsStorageDriver } from '@azversan/storage';
@Module({
imports: [
StorageModule.register({
name: 'media',
driverClass: GcsStorageDriver,
options: {
projectId: 'my-gcp-project',
bucket: 'my-media-bucket',
keyFilename: '/path/to/service-account.json',
publicRead: true,
},
}),
],
})
export class AppModule {}Registration Methods
register()
Register a single storage driver synchronously.
StorageModule.register({
name: 'avatars',
driverClass: LocalStorageDriver,
options: { dest: './uploads' },
});registerMany()
Register multiple drivers in a single call.
StorageModule.registerMany([
{
name: 'avatars',
driverClass: LocalStorageDriver,
options: { dest: './uploads/avatars' },
},
{
name: 'documents',
driverClass: S3StorageDriver,
options: { region: 'us-east-1', bucket: 'docs-bucket' },
},
]);registerAsync()
Register a driver asynchronously — useful when options come from ConfigService or another async provider.
useFactory:
StorageModule.registerAsync({
name: 'avatars',
driverClass: S3StorageDriver,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
region: config.get('AWS_REGION'),
bucket: config.get('S3_BUCKET'),
accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
}),
});useClass:
@Injectable()
class StorageConfigFactory implements StorageOptionsFactory<S3Options> {
constructor(private readonly config: ConfigService) {}
createStorageOptions(): S3Options {
return {
region: this.config.get('AWS_REGION'),
bucket: this.config.get('S3_BUCKET'),
};
}
}
StorageModule.registerAsync({
name: 'avatars',
driverClass: S3StorageDriver,
useClass: StorageConfigFactory,
});useExisting:
StorageModule.registerAsync({
name: 'avatars',
driverClass: S3StorageDriver,
imports: [ConfigFactoryModule],
useExisting: StorageConfigFactory,
});registerManyAsync()
Register multiple drivers asynchronously in a single call.
StorageModule.registerManyAsync([
{
name: 'avatars',
driverClass: LocalStorageDriver,
useFactory: () => ({ dest: './uploads/avatars' }),
},
{
name: 'backups',
driverClass: GcsStorageDriver,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
projectId: config.get('GCS_PROJECT_ID'),
bucket: config.get('GCS_BUCKET'),
}),
},
]);Injecting a Driver
Using @InjectStorage()
Inject a named driver directly into a constructor using the @InjectStorage() parameter decorator.
import { Injectable } from '@nestjs/common';
import { InjectStorage, LocalStorageDriver } from '@azversan/storage';
@Injectable()
export class AvatarService {
constructor(@InjectStorage('avatars') private readonly storage: LocalStorageDriver) {}
async uploadAvatar(userId: string, buffer: Buffer): Promise<string> {
const result = await this.storage.upload({
key: `avatars/${userId}.png`,
buffer,
mimeType: 'image/png',
});
return result.url ?? result.key;
}
}Using StorageRegistry
Resolve any registered driver by name at runtime using the StorageRegistry service.
import { Injectable } from '@nestjs/common';
import { StorageRegistry, S3StorageDriver } from '@azversan/storage';
@Injectable()
export class FileService {
constructor(private readonly storageRegistry: StorageRegistry) {}
getDriver(name: string) {
return this.storageRegistry.get<S3StorageDriver>(name);
}
}Note:
StorageRegistryis globally provided onceStorageModulehas been imported in your rootAppModule. No additional imports are needed in feature modules.
Driver API
All drivers extend StorageDriver and implement the following interface.
upload()
Upload a raw buffer to the storage backend.
const result = await storage.upload({
key: 'images/photo.jpg',
buffer: fileBuffer,
mimeType: 'image/jpeg',
metadata: { uploadedBy: 'user-42' },
});
// result: { key, driver, size, url? }delete()
Delete an object by its storage key.
await storage.delete('images/photo.jpg');exists()
Check whether an object exists at the given key. Returns true or false.
const found = await storage.exists('images/photo.jpg');getSignedUrl()
Generate a temporary pre-signed URL for private object access. Available on S3StorageDriver and GcsStorageDriver.
// S3: ttl in seconds (default: 900 = 15 min)
const url = await s3Driver.getSignedUrl('private/report.pdf', 300);
// GCS: ttl in milliseconds (default: 900_000 = 15 min)
const url = await gcsDriver.getSignedUrl('private/report.pdf', 300_000);Driver Reference
LocalOptions
| Option | Type | Required | Description |
| --------- | -------- | -------- | -------------------------------------------------------------- |
| dest | string | ✅ | Absolute or relative path where files will be stored on disk. |
| baseUrl | string | ❌ | Base URL prepended to the key to build a public URL on upload. |
S3Options
| Option | Type | Required | Description |
| ----------------- | --------- | -------- | ------------------------------------------------------------------------------------ |
| region | string | ✅ | AWS region (e.g. us-east-1). |
| bucket | string | ✅ | S3 bucket name. |
| accessKeyId | string | ❌ | AWS access key ID. Falls back to environment / IAM role if omitted. |
| secretAccessKey | string | ❌ | AWS secret access key. Falls back to environment / IAM role if omitted. |
| endpoint | string | ❌ | Custom endpoint for S3-compatible services (e.g. http://localhost:9000 for MinIO). |
| forcePathStyle | boolean | ❌ | Force path-style URLs. Required for MinIO. Defaults to false. |
| publicRead | boolean | ❌ | Tag uploads as public-read and return a permanent URL. Defaults to false. |
GcsOptions
| Option | Type | Required | Description |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------- |
| projectId | string | ✅ | GCP project ID. |
| bucket | string | ✅ | GCS bucket name. |
| keyFilename | string | ❌ | Path to a service-account key file. Use this or credentials, not both. |
| credentials | object | ❌ | Inline service-account credentials. Use this or keyFilename, not both. |
| publicRead | boolean | ❌ | Make uploaded objects world-readable and return a permanent URL. Defaults to false. |
Writing a Custom Driver
Extend StorageDriver to implement any backend you need.
import { StorageDriver, UploadOptions, UploadResult } from '@azversan/storage';
export interface MyOptions {
endpoint: string;
token: string;
}
export class MyCustomDriver extends StorageDriver<MyOptions> {
readonly driver = 'my-custom';
async upload(options: UploadOptions): Promise<UploadResult> {
// your upload logic here
return { key: options.key, driver: this.driver, size: options.buffer.length };
}
async delete(key: string): Promise<void> {
// your delete logic here
}
async exists(key: string): Promise<boolean> {
// your exists check here
return false;
}
}Then register it like any other driver:
StorageModule.register({
name: 'custom',
driverClass: MyCustomDriver,
options: { endpoint: 'https://my-storage.io', token: process.env.MY_TOKEN },
});Scripts
# Build the package
npm run build
# Build in watch mode
npm run build:watch
# Run all tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage report
npm run test:cov
# Run ESLint (with auto-fix)
npm run lint
# Format source files with Prettier
npm run formatProject Structure
src/
└── storage/
├── drivers/
│ ├── __tests__/
│ │ ├── gcs.driver.spec.ts
│ │ ├── local.driver.spec.ts
│ │ └── s3.driver.spec.ts
│ ├── gcs.driver.ts
│ ├── local.driver.ts
│ └── s3.driver.ts
├── services/
│ └── registry/
│ └── registry.service.ts
├── __tests__/
│ ├── storage.decorator.spec.ts
│ └── storage.module.spec.ts
├── storage.constants.ts
├── storage.decorator.ts
├── storage.interface.ts
├── storage.module.ts
└── storage.utils.tsSupport
If this project helps you or your team, please consider supporting its continued development. Your support helps maintain the package, improve documentation, and keep the project actively maintained. 🚀
Ways to Support
Thank you for supporting open-source software and helping this project grow ❤️
