npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devoven/storage

v0.1.1

Published

Storage Package for NestJS Hexagonal Architecture

Readme

@devoven/storage

Multi-cloud storage abstraction for NestJS. S3, GCS, and local filesystem behind a single port interface.

Installation

npm install @devoven/storage
# or
pnpm add @devoven/storage

Peer Dependencies

Standard NestJS peer dependencies (@nestjs/common, @nestjs/core, rxjs, reflect-metadata) must be present in your project. Any NestJS application already has these.

Install the SDK for whichever provider(s) you use. All cloud SDKs are optional peer dependencies:

# Amazon S3
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

# Google Cloud Storage
npm install @google-cloud/storage

LocalStorageProvider has no extra peer dependencies — no SDK install needed.

Quick Start

import { StorageModule, S3StorageProvider } from '@devoven/storage';

@Module({
  imports: [
    StorageModule.register({
      provider: new S3StorageProvider({
        bucket: 'my-bucket',
        region: 'us-east-1',
      }),
    }),
  ],
})
export class AppModule {}

Module Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | provider | StorageProviderPort instance | — | Required. The storage backend to use | | defaultSignedUrlTtl | number | 3600 | Default TTL in seconds for pre-signed URLs | | global | boolean | false | Register the module globally |

provider must be a pre-constructed instance (not a class). Instantiate the provider in your module or factory and pass the result.

Async Registration

Use registerAsync when the provider configuration comes from a config service:

import { StorageModule, S3StorageProvider } from '@devoven/storage';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    StorageModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        provider: new S3StorageProvider({
          bucket: config.get('S3_BUCKET'),
          region: config.get('S3_REGION'),
          credentials: {
            accessKeyId: config.get('AWS_ACCESS_KEY_ID'),
            secretAccessKey: config.get('AWS_SECRET_ACCESS_KEY'),
          },
        }),
        defaultSignedUrlTtl: config.get('SIGNED_URL_TTL'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Injecting the Provider

The module exports two tokens from TOKENS (both are Symbols):

| Token | Type | Description | |-------|------|-------------| | TOKENS.StorageProviderPort | StorageProviderPort | The configured storage backend | | TOKENS.DefaultSignedUrlTtl | number | The default TTL for signed URLs |

import { Inject, Injectable } from '@nestjs/common';
import { TOKENS, StorageProviderPort } from '@devoven/storage';
import { Readable } from 'stream';

@Injectable()
export class FileService {
  constructor(
    @Inject(TOKENS.StorageProviderPort)
    private readonly storage: StorageProviderPort,
    @Inject(TOKENS.DefaultSignedUrlTtl)
    private readonly defaultTtl: number,
  ) {}

  async uploadFile(key: string, buffer: Buffer, contentType: string) {
    return this.storage.upload(key, buffer, { contentType });
  }

  async getDownloadUrl(key: string): Promise<string> {
    return this.storage.getSignedDownloadUrl(key, this.defaultTtl);
  }
}

StorageProviderPort Interface

All provider operations are available through the single StorageProviderPort interface.

Basic operations

| Method | Signature | Description | |--------|-----------|-------------| | upload | (key, body, options?) => Promise<{ key }> | Upload a file. If the key already exists the provider appends a datetime suffix to avoid overwriting | | download | (key) => Promise<{ body: Readable, metadata: StorageObjectValueObject }> | Download as a stream with metadata | | delete | (key) => Promise<void> | Delete a single object | | deleteMany | (keys) => Promise<void> | Delete multiple objects in one call | | list | (prefix, options?) => Promise<ListResult> | List objects under a prefix with optional pagination |

Pre-signed URLs

| Method | Signature | Description | |--------|-----------|-------------| | getSignedUploadUrl | (key, expiresInSeconds, options?) => Promise<{ key, url }> | Generate a URL for client-side upload | | getSignedDownloadUrl | (key, expiresInSeconds) => Promise<string> | Generate a URL for direct client download |

Multipart uploads

Use multipart uploads for large files (typically >5 MB).

| Method | Signature | Description | |--------|-----------|-------------| | initiateMultipartUpload | (key, options?) => Promise<{ uploadId, key }> | Start a multipart upload session | | uploadPart | (key, uploadId, partNumber, body) => Promise<UploadedPart> | Upload one part | | completeMultipartUpload | (key, uploadId, parts) => Promise<void> | Finalise the upload | | abortMultipartUpload | (key, uploadId) => Promise<void> | Discard an incomplete multipart upload |

Multipart upload example

const PART_SIZE = 5 * 1024 * 1024; // 5 MB minimum per part (S3 requirement)

// 1. Start the session
const { uploadId, key } = await storage.initiateMultipartUpload('videos/demo.mp4', {
  contentType: 'video/mp4',
});

// 2. Upload each part (parts are 1-indexed)
const parts: UploadedPart[] = [];
for (let i = 0; i < chunks.length; i++) {
  const part = await storage.uploadPart(key, uploadId, i + 1, chunks[i]);
  parts.push(part);
}

// 3. Finalise — provider assembles the parts into a single object
await storage.completeMultipartUpload(key, uploadId, parts);

If an error occurs mid-upload, call abortMultipartUpload(key, uploadId) to release the incomplete upload and avoid storage charges.

UploadOptions

interface UploadOptions {
  contentType?: string;          // MIME type to store with the object
  cacheControl?: string;         // Cache-Control header value
  metadata?: Record<string, string>; // Custom key/value metadata
}

ListOptions

interface ListOptions {
  maxKeys?: number;              // Maximum results to return
  continuationToken?: string;    // Resume a paginated listing
}

ListResult

interface ListResult {
  objects: StorageObjectValueObject[]; // Objects found
  isTruncated?: boolean;               // Whether more results exist
  nextContinuationToken?: string;      // Pass to the next list call to continue
}

Built-in Providers

S3StorageProvider

Backed by @aws-sdk/client-s3. Supports all StorageProviderPort operations.

new S3StorageProvider({
  bucket: 'my-bucket',
  region: 'us-east-1',

  // Optional — defaults to AWS environment credentials
  credentials: {
    accessKeyId: 'AKIA...',
    secretAccessKey: '...',
  },

  // Optional — custom endpoint for MinIO or other S3-compatible stores
  endpoint: 'http://localhost:9000',
  forcePathStyle: true,
})

| Config field | Type | Default | Description | |---|---|---|---| | bucket | string | — | Required | | region | string | — | Required | | endpoint | string | AWS default | Custom endpoint URL | | credentials | { accessKeyId, secretAccessKey } | Environment | Explicit credentials | | forcePathStyle | boolean | false | Use path-style URLs (required for MinIO) |

GCSStorageProvider

Backed by @google-cloud/storage. Supports all StorageProviderPort operations.

new GCSStorageProvider({
  bucket: 'my-bucket',

  // Option A: inline credentials
  credentials: {
    clientEmail: '[email protected]',
    privateKey: '-----BEGIN PRIVATE KEY-----\n...',
  },

  // Option B: path to a service account JSON file
  keyFilename: '/path/to/service-account.json',

  projectId: 'my-gcp-project',
})

| Config field | Type | Default | Description | |---|---|---|---| | bucket | string | — | Required | | credentials | { clientEmail, privateKey } | ADC | Inline service account credentials | | projectId | string | From credentials | GCP project ID | | keyFilename | string | — | Path to a service account JSON key file |

LocalStorageProvider

Stores files on the local filesystem. Intended for development and testing. Pre-signed URLs are not supported — calls to getSignedUploadUrl and getSignedDownloadUrl throw UnsupportedOperation.

new LocalStorageProvider({
  basePath: './uploads',
})

| Config field | Type | Description | |---|---|---| | basePath | string | Required. Directory where files will be stored |

StorageObjectValueObject

Returned by download and list. Immutable value object.

| Property | Type | Description | |----------|------|-------------| | key | string | Object key (path within the bucket) | | extension | string \| undefined | File extension parsed from the key | | size | number \| undefined | File size in bytes | | contentType | string \| undefined | MIME type | | lastModified | Date \| undefined | Last modification timestamp | | etag | string \| undefined | Entity tag |

Created via StorageObjectValueObject.create({ key, size?, contentType?, lastModified?, etag? }).

Custom Adapters

Implement StorageProviderPort to add your own backend:

import { StorageProviderPort, UploadOptions, ListOptions, ListResult, UploadedPart } from '@devoven/storage';
import { StorageObjectValueObject } from '@devoven/storage';
import { Readable } from 'stream';

export class AzureBlobStorageProvider implements StorageProviderPort {
  async upload(key: string, body: Buffer | Readable, options?: UploadOptions) {
    // upload to Azure Blob Storage...
    return { key };
  }

  async download(key: string) {
    // download from Azure...
    return { body: readable, metadata: StorageObjectValueObject.create({ key }) };
  }

  async delete(key: string): Promise<void> { /* ... */ }
  async deleteMany(keys: string[]): Promise<void> { /* ... */ }
  async list(prefix: string, options?: ListOptions): Promise<ListResult> { /* ... */ }

  async getSignedUploadUrl(key: string, expiresInSeconds: number, options?: UploadOptions) {
    return { key, url: 'https://...' };
  }

  async getSignedDownloadUrl(key: string, expiresInSeconds: number): Promise<string> {
    return 'https://...';
  }

  async initiateMultipartUpload(key: string, options?: UploadOptions) {
    return { uploadId: '...', key };
  }

  async uploadPart(key: string, uploadId: string, partNumber: number, body: Buffer): Promise<UploadedPart> {
    return { partNumber, etag: '...' };
  }

  async completeMultipartUpload(key: string, uploadId: string, parts: UploadedPart[]): Promise<void> { /* ... */ }
  async abortMultipartUpload(key: string, uploadId: string): Promise<void> { /* ... */ }
}

Pass the instance as the provider option when registering the module:

StorageModule.register({
  provider: new AzureBlobStorageProvider(connectionString),
})

If a particular operation is not supported by your backend, throw UnsupportedOperation (exported from @devoven/storage) to return a 501 Not Implemented HTTP response.

Error Handling

UnsupportedOperation

UnsupportedOperation is an HttpException that maps to HTTP 501 Not Implemented. It is thrown when a storage operation is called on a provider that does not support it.

The built-in case is LocalStorageProvider: calling getSignedUploadUrl or getSignedDownloadUrl on it throws UnsupportedOperation because the local filesystem has no concept of pre-signed URLs.

import { UnsupportedOperation } from '@devoven/storage';

// In a custom adapter, signal that an operation is not available:
async getSignedDownloadUrl(key: string, expiresInSeconds: number): Promise<string> {
  throw new UnsupportedOperation('getSignedDownloadUrl');
}

When UnsupportedOperation is thrown inside a request handler, NestJS's default (or your custom) exception filter catches it and returns:

{
  "statusCode": 501,
  "message": "getSignedDownloadUrl",
  "error": "Unsupported Operation"
}

If no operation name is passed to the constructor, the message defaults to "Operation is not supported".

Consumers that call signed-URL methods conditionally based on provider type should catch UnsupportedOperation and fall back gracefully rather than letting it propagate as a 501 to the client.