@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 MiBThe /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.