Skip to content

Media Domain

The media domain provides media asset management with processing and variant generation for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add media

The MediaAsset entity is the aggregate root. It holds media metadata, dimensions (for images/videos), and a list of variants (thumbnails, previews, etc.).

import { MediaAsset } from "./domains/media/domain/entities/media-asset.entity";
const result = MediaAsset.create({
id: crypto.randomUUID(),
originalUrl: "uploads/photo.jpg",
mimeType: "image/jpeg",
width: 1920,
height: 1080,
size: 204800,
});
if (result.isOk()) {
const asset = result.unwrap();
console.log(asset.id, asset.mimeType.value, asset.dimensions?.width);
}
FieldTypeDescription
idstringUnique identifier (UUID)
originalUrlstringURL or path to the original file
mimeTypeMimeTypeValidated MIME type with category
dimensionsDimensions | nullWidth and height (null for non-visual media)
sizenumberFile size in bytes (positive integer)
variantsReadonlyArray<MediaVariant>Generated variants (thumbnails, previews, etc.)
uploadedAtDateTimestamp of upload

MediaAsset.create() returns Result<MediaAsset, UnsupportedFormat | FileTooLarge | Error>. The entity is immutable — addVariant() and withVariants() return new instances.

import { MediaVariant } from "./domains/media/domain/entities/media-variant.entity";
const result = MediaVariant.create({
id: crypto.randomUUID(),
url: "uploads/photo-thumb.jpg",
width: 150,
height: 150,
format: "jpeg",
purpose: "thumbnail",
});

Validates against a supported list and categorizes as image, video, or document.

import { MimeType } from "./domains/media/domain/value-objects/mime-type.vo";
const result = MimeType.create("image/jpeg");
if (result.isOk()) {
const mime = result.unwrap();
console.log(mime.category); // "image"
console.log(mime.isImage()); // true
}

Supported types include: image/jpeg, image/png, image/gif, image/webp, image/svg+xml, image/avif, video/mp4, video/webm, video/quicktime, application/pdf, and more.

Width and height as positive integers with aspect ratio computation.

import { Dimensions } from "./domains/media/domain/value-objects/dimensions.vo";
const dims = Dimensions.create(1920, 1080).unwrap();
console.log(dims.aspectRatio); // ~1.778 (16:9)

Enum value object: thumbnail, preview, original, optimized.

import { MediaPurpose } from "./domains/media/domain/value-objects/media-purpose.vo";
const purpose = MediaPurpose.thumbnail();
const fromString = MediaPurpose.from("preview"); // Result<MediaPurpose, Error>
Error ClassConditionMessage
UnsupportedFormatMIME type not in supported listUnsupported media format: "<mimeType>"
MediaNotFoundNo media found for the given IDMedia not found with id: "<id>"
ProcessingFailedVariant generation failedMedia processing failed for "<id>": <reason>
FileTooLargeFile exceeds maximum allowed sizeFile size <size> bytes exceeds maximum allowed size of <maxSize> bytes
EventEmitted ByPayload
MediaUploadedUploadMedia use casemediaId, name, mimeType, size, occurredAt
MediaProcessedProcessMedia use casemediaId, variantCount, occurredAt
MediaDeleted— (available for event consumers)mediaId, occurredAt

Creates a new media asset, validates MIME type and size, persists it, and emits a MediaUploaded event.

const result = await uploadMedia.execute({
name: "photo.jpg",
originalUrl: "uploads/photo.jpg",
mimeType: "image/jpeg",
size: 204800,
width: 1920,
height: 1080,
});
// Result<{ output: { mediaId: string }; event: MediaUploaded }, Error>

Generates variants (thumbnails, previews) using the IMediaProcessor port.

const result = await processMedia.execute({
mediaId: "abc-123",
variants: [
{ purpose: "thumbnail", width: 150, height: 150, format: "jpeg" },
{ purpose: "preview", width: 800, height: 600, format: "webp" },
],
});
// Result<{ output: { mediaId, variantCount }; event: MediaProcessed }, Error>

GetMedia / ListMedia / DeleteMedia / GetMediaUrl

Section titled “GetMedia / ListMedia / DeleteMedia / GetMediaUrl”
const media = await getMedia.execute({ mediaId: "abc-123" });
const list = await listMedia.execute({ limit: 20, offset: 0 });
const deleted = await deleteMedia.execute({ mediaId: "abc-123" });
const url = await getMediaUrl.execute({ mediaId: "abc-123", purpose: "thumbnail" });
export interface FindAllOptions {
limit?: number;
offset?: number;
}
export interface IMediaRepository {
save(asset: MediaAsset): Promise<void>;
findById(mediaId: string): Promise<MediaAsset | null>;
findAll(options?: FindAllOptions): Promise<MediaAsset[]>;
delete(mediaId: string): Promise<void>;
}
export interface IMediaProcessor {
resize(inputUrl: string, width: number, height: number): Promise<ProcessedOutput>;
convert(inputUrl: string, format: string): Promise<ProcessedOutput>;
generateThumbnail(inputUrl: string, size: number): Promise<ProcessedOutput>;
}
export interface IMediaStorage {
upload(key: string, data: Buffer): Promise<void>;
download(key: string): Promise<Buffer>;
delete(key: string): Promise<void>;
getUrl(key: string): Promise<string>;
}
import { createMediaService, IMediaService } from "./domains/media/contracts";
const mediaService: IMediaService = createMediaService({
mediaRepository,
mediaProcessor,
mediaStorage,
});
// IMediaService interface:
// upload(input): Promise<Result<{ output: UploadMediaOutput; event: MediaUploaded }, Error>>
// process(input): Promise<Result<{ output: ProcessMediaOutput; event: MediaProcessed }, Error>>
// get(input): Promise<Result<GetMediaOutput, Error>>
// list(input): Promise<Result<ListMediaOutput, Error>>
// delete(input): Promise<Result<{ event: MediaDeleted }, Error>>
// getUrl(input): Promise<Result<GetMediaUrlOutput, Error>>
  • files = raw upload/download/delete — no processing, no variants, no metadata enrichment
  • media = processing-aware — thumbnails, format conversion, dimensions, variants, CDN URLs
  • When both are installed, you can wire media’s IMediaStorage port to delegate raw storage to files’ IFileStorage
domains/media/
domain/
entities/media-asset.entity.ts
entities/media-variant.entity.ts
value-objects/mime-type.vo.ts
value-objects/dimensions.vo.ts
value-objects/media-purpose.vo.ts
errors/unsupported-format.error.ts
errors/media-not-found.error.ts
errors/processing-failed.error.ts
errors/file-too-large.error.ts
events/media-uploaded.event.ts
events/media-processed.event.ts
events/media-deleted.event.ts
application/
use-cases/upload-media.use-case.ts
use-cases/process-media.use-case.ts
use-cases/get-media.use-case.ts
use-cases/list-media.use-case.ts
use-cases/delete-media.use-case.ts
use-cases/get-media-url.use-case.ts
ports/media-repository.port.ts
ports/media-processor.port.ts
ports/media-storage.port.ts
dto/upload-media.dto.ts
dto/process-media.dto.ts
dto/get-media.dto.ts
dto/list-media.dto.ts
dto/delete-media.dto.ts
dto/get-media-url.dto.ts
contracts/
media.contract.ts
media.factory.ts
index.ts
shared/
result.ts