Aviary
Packages

@dudousxd/nestjs-media

The umbrella package — MediaModule, MediaService, the tus controller, and the /storage subpath.

@dudousxd/nestjs-media is the package you import in a Nest app. It wires core into NestJS DI and exposes everything through one service.

MediaModule

forRoot / forRootAsync register a @Global() module. It always provides the storage layer; pass store for the media-library, uploadSessions for resumable uploads, tus to mount the proxy HTTP controller, and direct to mount the direct presigned-multipart controller. See Reference → Configuration for every option.

MediaModule.forRoot({
  default: 'local',
  disks: { local },
  store,                  // → media.library
  uploadSessions,         // → media.uploads
  tus: { disk: 'local' }, // → mounts MediaUploadController
  direct: { disk: 's3' }, // → mounts MediaDirectUploadController (S3)
});

MediaService

The single injectable surface:

class MediaService {
  disk(name?: string): StorageDriver;        // layer 1
  get library(): MediaLibrary;               // layer 2, table model (throws if no store)
  get attachments(): AttachmentManager;      // layer 2, column model (always available)
  get uploads(): ResumableUploadManager;     // resumable proxy (throws if no uploadSessions)
  get directUploads(): DirectUploadManager;  // direct presigned multipart (throws if no direct)
}

media.attachments is the attachment column model and is always provided — it only needs storage (and an image processor for variants), no store or table. media.library is the media table and throws until you pass a store.

@Injectable()
export class Files {
  constructor(private readonly media: MediaService) {}
  save = (p: string, b: Buffer) => this.media.disk('s3').put(p, b);
}

tus controller

When tus is configured, MediaUploadController is mounted at media/uploads, implementing the tus protocol over the resumable engine. Remember the raw-body parser:

app.use('/media/uploads', express.raw({ type: 'application/offset+octet-stream' }));

Uploads are unauthenticated unless you set `guards`

MediaUploadController (tus), MediaMultipartUploadController, and MediaDirectUploadController carry no guard by default. Pass guards to MediaModule.forRoot/forRootAsync to gate them — a plain array gates all three uniformly, or a per-surface object gates each independently, since the surfaces often carry different sensitivity (session creation admin-only, part PUTs any authenticated user):

MediaModule.forRoot({
  // …
  guards: { tus: [AdminGuard], multipart: [AuthenticatedGuard] }, // direct omitted → unguarded
});

An omitted key (or the whole option) leaves that surface unguarded — uploads remain unauthenticated by default. On forRootAsync, guards is a static field on the config object, not resolved via useFactory, since controllers are wired at module-build time; a guard that needs async-resolved config (e.g. a ConfigService secret) should inject it itself via DI, with its providing module passed through imports.

Mounting only what you use

forRootAsync mounts all three upload controllers unconditionally by default — each one 501s if you never configure its feature in the factory — since Nest wires controllers at module-build time, before the async factory runs. Pass a static mount object to drop the ones you never use entirely, so they 404 instead of existing as dead, always-501 surface:

MediaModule.forRootAsync({
  useFactory: () => ({ /* … */ }),
  mount: { direct: false }, // no direct-upload config → don't mount the controller at all
});

forRoot doesn't need this: it already only mounts a controller when the matching option (tus/direct) is present.

Direct upload controller

When direct is configured (with a presign/multipart-capable disk like S3), MediaDirectUploadController is mounted at media/uploads/direct, orchestrating browser-to-S3 presigned multipart uploads over DirectUploadManager (media.directUploads). It exposes initiate / per-part presign / complete / abort — no raw-body parser is needed, since the bytes go straight to S3.

direct: { disk: 's3', partSize: 8 * 1024 * 1024 } // partSize optional, default 8 MiB

The /storage subpath

For consumers that want only the filesystem facade, without the media-library layer:

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

Injection tokens

import {
  MEDIA_STORAGE, MEDIA_LIBRARY, MEDIA_ATTACHMENTS, MEDIA_UPLOADS, MEDIA_TUS, MEDIA_DIRECT,
} from '@dudousxd/nestjs-media';

Inject MediaService for normal use; reach for the tokens only when you need the raw StorageManager/MediaLibrary/ResumableUploadManager/DirectUploadManager (e.g. wiring a custom controller).

Facade re-exports

@dudousxd/nestjs-media's entrypoint also re-exports the error classes (FileNotFoundError, etc.), ResumableUploadManager, mediaDiagnosticKey, MediaDiagnosticEvent, publishMedia, and the storage-consumer/upload-session types (StatResult, TemporaryUrlOptions, ListResult, ListEntry, ListOptions, MultipartPart, UploadSession, UploadSessionStore, UploadSessionListFilter, CreateUploadInput) from @dudousxd/nestjs-media-core — so a consumer app doesn't need its own dependency on -core just to reach these:

import {
  FileNotFoundError, ResumableUploadManager, mediaDiagnosticKey, publishMedia,
  type MediaDiagnosticEvent, type UploadSession, type UploadSessionStore,
} from '@dudousxd/nestjs-media';

Peer dependencies

@nestjs/common and @nestjs/core (v10, v11 or v12), reflect-metadata, and @dudousxd/nestjs-media-core. Disks, stores, and the image engine are separate installs.

On this page