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
- 🗂️ 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
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/storageimport { 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 {}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',
},
});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 {}Register a single storage driver synchronously.
StorageModule.register({
name: 'avatars',
driverClass: LocalStorageDriver,
options: { dest: './uploads' },
});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' },
},
]);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,
});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'),
}),
},
]);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;
}
}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.
All drivers extend StorageDriver and implement the following interface.
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 an object by its storage key.
await storage.delete('images/photo.jpg');Check whether an object exists at the given key. Returns true or false.
const found = await storage.exists('images/photo.jpg');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);| 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. |
| 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. |
| 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. |
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 },
});# 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 formatsrc/
└── 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.ts
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. 🚀
- 💖 GitHub Sponsors
- ☕ Buy Me a Coffee
- 🇮🇩 Saweria
- 🎁 Sociabuzz
- 💳 Trakteer
Thank you for supporting open-source software and helping this project grow ❤️