Skip to content

Files Domain

The files domain provides file upload, retrieval, and deletion 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 files

The File entity is the aggregate root of the files domain. It holds file metadata including a validated path.

import { File } from "./domains/files/domain/entities/file.entity";
const result = File.create({
id: crypto.randomUUID(),
name: "photo.jpg",
path: "uploads/photo.jpg",
mimeType: "image/jpeg",
size: 204800,
});
if (result.isOk()) {
const file = result.unwrap();
console.log(file.id, file.name, file.path.value, file.size);
}
FieldTypeDescription
idstringUnique identifier (UUID)
namestringOriginal file name
pathFilePathValidated path value object
mimeTypestringMIME type (e.g. image/jpeg)
sizenumberFile size in bytes (must be a positive integer)
uploadedAtDateTimestamp of upload

File.create() returns Result<File, InvalidFilePath | FileTooLarge>. Zero-byte files are rejected.

import { FilePath } from "./domains/files/domain/value-objects/file-path.vo";
const result = FilePath.create("uploads/photo.jpg");
// Result<FilePath, InvalidFilePath>
if (result.isOk()) {
const path = result.unwrap();
console.log(path.value); // "uploads/photo.jpg"
}

Validates: non-empty, no .. path traversal, matches safe pattern ^[a-zA-Z0-9._\-][a-zA-Z0-9._\-/]*$.

Error ClassConditionMessage
FileNotFoundNo file found for the given IDFile not found with id: "<id>"
InvalidFilePathPath fails format or traversal validationInvalid file path: "<path>"
FileTooLargeFile exceeds maximum allowed sizeFile size <size> bytes exceeds maximum allowed size of <maxSize> bytes
EventEmitted ByPayload
FileUploadedUploadFile use casefileId, name, mimeType, size, occurredAt

Creates a new file entity, validates it, persists it, and emits a FileUploaded event.

import { UploadFile } from "./domains/files/application/use-cases/upload-file.use-case";
const uploadFile = new UploadFile(fileStorage);
const result = await uploadFile.execute({
name: "photo.jpg",
path: "uploads/photo.jpg",
mimeType: "image/jpeg",
size: 204800,
});
// Result<{ output: { fileId: string }; event: FileUploaded }, Error>

Possible failures: InvalidFilePath, FileTooLarge

Retrieves a file by ID.

import { GetFile } from "./domains/files/application/use-cases/get-file.use-case";
const getFile = new GetFile(fileStorage);
const result = await getFile.execute({ fileId: "abc-123" });
// Result<GetFileOutput, FileNotFound>

Deletes a file by ID.

import { DeleteFile } from "./domains/files/application/use-cases/delete-file.use-case";
const deleteFile = new DeleteFile(fileStorage);
const result = await deleteFile.execute({ fileId: "abc-123" });
// Result<void, FileNotFound>
export interface IFileStorage {
save(file: File): Promise<void>;
findById(fileId: string): Promise<File | null>;
delete(fileId: string): Promise<void>;
}
import { createFilesService, IFilesService } from "./domains/files/contracts";
const filesService: IFilesService = createFilesService({
fileStorage,
});
// IFilesService interface:
// upload(input: UploadFileInput): Promise<Result<UploadFileOutput, Error>>
// get(input: GetFileInput): Promise<Result<GetFileOutput, Error>>
// delete(input: DeleteFileInput): Promise<Result<void, Error>>

This is the only import consumers need. The internal use case classes are implementation details.

domains/files/
domain/
entities/file.entity.ts
value-objects/file-path.vo.ts
errors/file-not-found.error.ts
errors/invalid-file-path.error.ts
errors/file-too-large.error.ts
events/file-uploaded.event.ts
application/
use-cases/upload-file.use-case.ts
use-cases/get-file.use-case.ts
use-cases/delete-file.use-case.ts
ports/file-storage.port.ts
dto/upload-file.dto.ts
dto/get-file.dto.ts
dto/delete-file.dto.ts
contracts/
files.contract.ts
files.factory.ts
index.ts
shared/
result.ts