Aviary
Recipes

Raw storage (no entities)

Use the filesystem layer alone — for generated files, caches, or other libraries.

Not every file belongs to a record. Generated reports, rendered caches, exports, temp artifacts — these want a filesystem, not a media library. Register MediaModule with just disks (no store) and use media.disk(...) directly.

Minimal setup

MediaModule.forRoot({
  default: 'local',
  disks: {
    local: new LocalDriver({ root: './storage' }),
    s3: new S3Driver({ client: s3, bucket: 'app-files' }),
  },
});

Write, read, sign

export.service.ts
@Injectable()
export class ExportService {
  constructor(private readonly media: MediaService) {}

  async writeReport(id: string, csv: Buffer): Promise<string> {
    await this.media.disk('s3').put(`reports/${id}.csv`, csv, { contentType: 'text/csv' });
    // a 5-minute signed link the user can download
    return this.media.disk('s3').temporaryUrl(`reports/${id}.csv`, 300);
  }

  streamReport(id: string) {
    return this.media.disk('s3').stream(`reports/${id}.csv`); // pipe to the HTTP response
  }

  async purgeOld(id: string) {
    await this.media.disk('s3').delete(`reports/${id}.csv`); // idempotent
  }
}

Prefer stream over get for anything large — it pipes without buffering the whole file in memory.

Consuming from another library

This is exactly why media absorbed storage instead of shipping a separate package. A library that needs a filesystem (a mail channel attaching files, a durable workflow step staging a big upload) imports the facade through the subpath — no media-library layer, no NestJS coupling:

import { StorageManager, type StorageDriver } from '@dudousxd/nestjs-media/storage';

export class MailAttachments {
  constructor(private readonly storage: StorageManager) {}
  read(path: string) {
    return this.storage.disk().stream(path);
  }
}

Calling media.library without a configured store throws a clear error — the two layers are independent, and the storage layer never requires a database.

On this page