Backend File Upload
No file entity table — URLs stored as string columns (imageUrl, coverUrl).
Controller
@ApiConsumes('multipart/form-data')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'imageUrl', maxCount: 1 },
{ name: 'coverUrl', maxCount: 1 },
]))
@Post('create-category')
async create(
@UploadedFiles() files: { imageUrl?: MemoryStorageFile[]; coverUrl?: MemoryStorageFile[] },
@Body() dto: CreateCategoryDto,
) {
const imageFile = files?.imageUrl?.[0];
const coverFile = files?.coverUrl?.[0];
return sendSuccessfulResponse(await this.service.create(dto, imageFile, coverFile));
}
DTO fields use @RequiredFileField('image') / @OptionalFileField('cover').
Two-Phase Save (create)
1. prisma.create({ ...data, imageUrl: null }) — record without URL
2. folder = `{plural}/{singular}-{id}` — e.g. categories/category-{uuid}
3. files = storage.addPathToFiles([file], folder) — UUID path + relative URL
4. if no files → delete record → throw BAD_REQUEST
5. prisma.update({ imageUrl: files[0].fileurl })
6. storage.saveFilesOnServer(files) — write disk
7. on save failure → throw (record may need cleanup)
Update (optional new file)
if (file) {
const files = await this.storage.addPathToFiles([file], `${folder}`);
data.imageUrl = files[0].fileurl;
await this.prisma.update({ where: { id }, data });
await this.storage.saveFilesOnServer(files);
if (existing.imageUrl) await this.storage.deleteFile(existing.imageUrl);
}
StorageService Methods
| Method | Purpose |
|---|---|
addPathToFiles(files, folder) |
Assign UUID filename + URL |
saveFilesOnServer(files) |
Write buffers to disk |
deleteFile(url) |
Remove file |
getClientFiles(files) |
Normalize multipart input |
URL Format
/{folder}/{uuid}-{timestamp}-{fieldname}.{ext} served from uploads/ via @fastify/static.
Multipart Config (main.ts)
Max 500MB, 10 files. LIMIT_FILE_SIZE → HTTP 412.
Module
Import StorageModule in feature module. @Global() — inject StorageService anywhere.