Skip to content

Configuration

StorageModule.forRoot()

The simplest way to configure storage is with forRoot(), passing a StorageConfig object directly:

typescript
import { Module } from '@nestjs/common';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    StorageModule.forRoot({
      default: 'local',
      disks: {
        local: {
          driver: 'local',
          root: './storage/app',
          url: 'http://localhost:3000/files',
        },
      },
    }),
  ],
})
export class AppModule {}

TIP

StorageModule sets isGlobal: true by default, meaning you only need to import it once in your root AppModule. Every other module in your application can inject StorageService without importing StorageModule again.

StorageConfig Interface

PropertyTypeDefaultDescription
defaultstring'local'Name of the default disk. Used when calling storage.disk() with no arguments.
disksRecord<string, DiskConfig>{}Map of disk names to their configuration objects.
auditLogbooleanfalseWhen true, enables the StorageAuditService to record all storage operations.

DiskConfig Interface

Each disk in the disks map is configured with a DiskConfig object. The available fields depend on the driver.

Common Fields (All Drivers)

PropertyTypeDefaultDescription
driverstring--Required. One of: 'local', 's3', 'r2', 'gcs', 'azure', 'minio', 'b2', 'do', 'wasabi'
rootstring'.'Root path or prefix for all operations on this disk.
urlstringundefinedBase URL for generating public file URLs.
visibility'public' | 'private''private'Default visibility for new files.
throwbooleantrueWhen false, read operations return null instead of throwing StorageFileNotFoundError.
namingStrategyNamingStrategyundefinedDefault naming strategy for putFile() / putFileAs().
cdnCdnConfigundefinedCDN configuration. When present, wraps the disk in a CdnDisk.

S3-Compatible Fields (s3, r2, minio, b2, do, wasabi)

PropertyTypeDefaultDescription
bucketstring--Required. The S3 bucket name.
regionstring'us-east-1'AWS region.
keystringundefinedAWS access key ID. Falls back to SDK credential chain.
secretstringundefinedAWS secret access key. Falls back to SDK credential chain.
endpointstringundefinedCustom endpoint URL (required for R2, MinIO, B2, DO, Wasabi).
use_path_style_endpointbooleanfalseUse path-style addressing (endpoint/bucket) instead of virtual-hosted. Required for MinIO.
signSecretstringundefinedHMAC secret for signed URLs (LocalDisk only).

GCS Fields

PropertyTypeDefaultDescription
bucketstring--Required. The GCS bucket name.
projectIdstringundefinedGCP project ID.
keyFilenamestringundefinedPath to service account JSON key file.

Azure Fields

PropertyTypeDefaultDescription
bucketstring--Required. The Azure container name.
connectionStringstringundefinedAzure storage connection string.
accountNamestringundefinedAzure storage account name (alternative to connection string).
accountKeystringundefinedAzure storage account key.

CDN Sub-Config

PropertyTypeDefaultDescription
cdn.baseUrlstring--Required. CDN base URL (e.g., 'https://cdn.example.com').
cdn.provider'cloudfront' | 'generic''generic'CDN provider. 'cloudfront' enables CloudFront signed URLs.
cdn.signingKeyIdstringundefinedCloudFront key pair ID (required when provider is 'cloudfront').
cdn.signingKeystringundefinedCloudFront private key PEM string (required when provider is 'cloudfront').

StorageModule.forRootAsync()

For dynamic configuration -- injecting ConfigService, connecting to a database, or fetching secrets at startup -- use forRootAsync().

useFactory

The most common pattern. Inject any NestJS providers into the factory function:

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    ConfigModule.forRoot(),
    StorageModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        default: config.get('STORAGE_DEFAULT', 'local'),
        disks: {
          local: {
            driver: 'local',
            root: config.get('STORAGE_LOCAL_ROOT', './storage'),
            url: config.get('STORAGE_LOCAL_URL', 'http://localhost:3000/files'),
          },
          s3: {
            driver: 's3',
            bucket: config.get('AWS_BUCKET'),
            region: config.get('AWS_REGION', 'us-east-1'),
            key: config.get('AWS_ACCESS_KEY_ID'),
            secret: config.get('AWS_SECRET_ACCESS_KEY'),
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

useClass

Encapsulate configuration logic in a dedicated class:

typescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  StorageModule,
  StorageOptionsFactory,
  StorageConfig,
} from '@fozooni/nestjs-storage';

@Injectable()
class StorageConfigService implements StorageOptionsFactory {
  constructor(private readonly config: ConfigService) {}

  createStorageOptions(): StorageConfig {
    const isProduction = this.config.get('NODE_ENV') === 'production';

    return {
      default: isProduction ? 's3' : 'local',
      disks: {
        local: {
          driver: 'local',
          root: './storage',
          url: this.config.get('APP_URL') + '/files',
        },
        s3: {
          driver: 's3',
          bucket: this.config.get('AWS_BUCKET'),
          region: this.config.get('AWS_REGION'),
          key: this.config.get('AWS_ACCESS_KEY_ID'),
          secret: this.config.get('AWS_SECRET_ACCESS_KEY'),
          cdn: isProduction
            ? {
                baseUrl: this.config.get('CDN_URL'),
                provider: 'cloudfront',
                signingKeyId: this.config.get('CF_KEY_ID'),
                signingKey: this.config.get('CF_PRIVATE_KEY'),
              }
            : undefined,
        },
      },
    };
  }
}

@Module({
  imports: [
    ConfigModule.forRoot(),
    StorageModule.forRootAsync({
      imports: [ConfigModule],
      useClass: StorageConfigService,
    }),
  ],
})
export class AppModule {}

useExisting

Reference a provider already registered in another module:

typescript
@Module({
  imports: [
    ConfigModule.forRoot(),
    SharedModule, // exports StorageConfigService
    StorageModule.forRootAsync({
      useExisting: StorageConfigService,
    }),
  ],
})
export class AppModule {}

Injecting Specific Disks

Using StorageService

The most flexible approach -- select disks at runtime:

typescript
import { Injectable } from '@nestjs/common';
import { StorageService } from '@fozooni/nestjs-storage';

@Injectable()
export class DocumentsService {
  constructor(private readonly storage: StorageService) {}

  async save(content: Buffer) {
    // Use default disk
    await this.storage.disk().put('doc.pdf', content);

    // Use named disk
    await this.storage.disk('s3').put('doc.pdf', content);
  }
}

Using @InjectDisk()

For services that always operate on a specific disk, inject it directly with the injectDisks option:

typescript
// app.module.ts
StorageModule.forRoot({
  default: 'local',
  injectDisks: ['s3', 'gcs'],
  disks: {
    local: { driver: 'local', root: './storage' },
    s3: { driver: 's3', bucket: 'my-bucket', region: 'us-east-1' },
    gcs: { driver: 'gcs', bucket: 'my-gcs-bucket' },
  },
})
typescript
// uploads.service.ts
import { Injectable } from '@nestjs/common';
import { InjectDisk, FilesystemContract } from '@fozooni/nestjs-storage';

@Injectable()
export class UploadsService {
  constructor(
    @InjectDisk('s3') private readonly s3: FilesystemContract,
  ) {}

  async upload(path: string, content: Buffer) {
    await this.s3.put(path, content);
    return this.s3.url(path);
  }
}

WARNING

Disk names passed to injectDisks must match keys in the disks config. If a disk name is not listed in injectDisks, @InjectDisk() will throw at startup because no provider was registered for that token.

Using @InjectStorage()

Injects the StorageService instance (equivalent to constructor injection but useful in edge cases):

typescript
import { InjectStorage, StorageService } from '@fozooni/nestjs-storage';

@Injectable()
export class MyService {
  constructor(
    @InjectStorage() private readonly storage: StorageService,
  ) {}
}

Environment-Driven Configuration

A production-ready pattern using @nestjs/config with validation:

typescript
// storage.config.ts
import { registerAs } from '@nestjs/config';

export const storageConfig = registerAs('storage', () => ({
  default: process.env.STORAGE_DRIVER || 'local',
  localRoot: process.env.STORAGE_LOCAL_ROOT || './storage',
  localUrl: process.env.STORAGE_LOCAL_URL || 'http://localhost:3000/files',
  s3Bucket: process.env.AWS_S3_BUCKET,
  s3Region: process.env.AWS_REGION || 'us-east-1',
  s3Key: process.env.AWS_ACCESS_KEY_ID,
  s3Secret: process.env.AWS_SECRET_ACCESS_KEY,
  cdnUrl: process.env.CDN_BASE_URL,
}));
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';
import { storageConfig } from './storage.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [storageConfig],
    }),
    StorageModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        default: config.get('storage.default'),
        disks: {
          local: {
            driver: 'local',
            root: config.get('storage.localRoot'),
            url: config.get('storage.localUrl'),
          },
          s3: {
            driver: 's3',
            bucket: config.get('storage.s3Bucket'),
            region: config.get('storage.s3Region'),
            key: config.get('storage.s3Key'),
            secret: config.get('storage.s3Secret'),
            cdn: config.get('storage.cdnUrl')
              ? { baseUrl: config.get('storage.cdnUrl') }
              : undefined,
          },
        },
      }),
    }),
  ],
})
export class AppModule {}
bash
# .env
STORAGE_DRIVER=s3
AWS_S3_BUCKET=my-production-bucket
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
CDN_BASE_URL=https://cdn.example.com

Multi-Disk Configuration

Here is a full example with local, S3, and R2 configured simultaneously:

typescript
StorageModule.forRoot({
  default: 'local',
  disks: {
    // Local filesystem for development and temporary files
    local: {
      driver: 'local',
      root: './storage/app',
      url: 'http://localhost:3000/files',
      visibility: 'public',
    },

    // S3 for primary cloud storage
    s3: {
      driver: 's3',
      bucket: 'my-app-primary',
      region: 'us-east-1',
      key: process.env.AWS_ACCESS_KEY_ID,
      secret: process.env.AWS_SECRET_ACCESS_KEY,
      visibility: 'private',
      cdn: {
        baseUrl: 'https://d1234.cloudfront.net',
        provider: 'cloudfront',
        signingKeyId: process.env.CF_KEY_ID,
        signingKey: process.env.CF_PRIVATE_KEY,
      },
    },

    // Cloudflare R2 for cost-effective egress
    r2: {
      driver: 'r2',
      bucket: 'my-app-public',
      key: process.env.R2_ACCESS_KEY_ID,
      secret: process.env.R2_SECRET_ACCESS_KEY,
      endpoint: process.env.R2_ENDPOINT,
      visibility: 'public',
    },

    // Temporary disk for processing
    temp: {
      driver: 'local',
      root: '/tmp/my-app',
      throw: false, // Returns null instead of throwing when files are missing
    },
  },
})

INFO

You can have multiple disks using the same driver with different configurations. In the example above, local and temp both use the 'local' driver but point to different root directories with different behaviors.

Driver-Specific Endpoint Examples

Cloudflare R2

typescript
{
  driver: 'r2',
  bucket: 'my-bucket',
  key: process.env.R2_ACCESS_KEY_ID,
  secret: process.env.R2_SECRET_ACCESS_KEY,
  endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
}

MinIO (Self-Hosted)

typescript
{
  driver: 'minio',
  bucket: 'my-bucket',
  region: 'us-east-1',
  key: 'minioadmin',
  secret: 'minioadmin',
  endpoint: 'http://localhost:9000',
  use_path_style_endpoint: true,
}

Backblaze B2

typescript
{
  driver: 'b2',
  bucket: 'my-bucket',
  region: 'us-west-004',
  key: process.env.B2_APPLICATION_KEY_ID,
  secret: process.env.B2_APPLICATION_KEY,
  endpoint: `https://s3.${process.env.B2_REGION}.backblazeb2.com`,
}

DigitalOcean Spaces

typescript
{
  driver: 'do',
  bucket: 'my-space',
  region: 'nyc3',
  key: process.env.DO_SPACES_KEY,
  secret: process.env.DO_SPACES_SECRET,
  endpoint: 'https://nyc3.digitaloceanspaces.com',
}

Wasabi

typescript
{
  driver: 'wasabi',
  bucket: 'my-bucket',
  region: 'us-east-1',
  key: process.env.WASABI_ACCESS_KEY,
  secret: process.env.WASABI_SECRET_KEY,
  endpoint: 'https://s3.wasabisys.com',
}

Next Steps

With your storage configured, you are ready to start performing Core Operations -- reading, writing, deleting, and streaming files.

Released under the MIT License.