Single-file avatar
A single-file collection that replaces the previous image on each upload, with MIME validation.
The classic "one image per user, replaced on re-upload" case is a single collection. Attaching a new file deletes the previous record and its bytes automatically, so there's never a second avatar lying around.
Configure the collection
collections: [
{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg', 'image/webp'] },
]single gives you the replace-on-attach behavior; acceptsMimeTypes rejects anything else with MimeNotAllowedError before a byte is written, so a bad upload never touches the disk.
The controller + service
@Controller('users/:id/avatar')
export class AvatarController {
constructor(private readonly media: MediaService) {}
@Put()
@UseInterceptors(FileInterceptor('file'))
setAvatar(@Param('id') userId: string, @UploadedFile() file: Express.Multer.File) {
return this.media.library.attach({
ownerType: 'User',
ownerId: userId,
collection: 'avatar',
fileName: file.originalname,
mimeType: file.mimetype,
contents: file.buffer,
customProperties: { uploadedBy: userId },
});
}
}async avatarUrl(userId: string): Promise<string | null> {
const [avatar] = await this.media.library.list('User', userId, 'avatar');
return avatar ? this.media.library.url(avatar.id) : null;
}list on a single-file collection returns zero or one record, so destructuring the first element is safe.
Add a thumbnail
Want a 64px avatar for nav bars? Add a conversion and request it — generated lazily, cached forever:
{ name: 'avatar', single: true, conversions: [{ name: 'sm', width: 64 }] }await this.media.library.url(avatar.id, 'sm');customProperties is free-form JSON stored on the record — handy for things like the original uploader, alt text, or a moderation flag. It round-trips untouched.
Or: skip the table entirely
An avatar is the textbook case for the attachment column model — one file, one field, no separate media table. The avatar is a column on your User:
async setAvatar(user: User, file: Express.Multer.File) {
if (user.avatar) await this.media.attachments.delete(user.avatar); // drop the old one + variants
user.avatar = await this.media.attachments.createFromFile(
{ fileName: file.originalname, mimeType: file.mimetype, contents: file.buffer },
{ variants: [{ name: 'sm', width: 64 }] },
);
await this.users.save(user);
}
avatarUrl(user: User) {
return user.avatar ? this.media.attachments.url(user.avatar, 'sm') : null;
}No collection config, no list, no ownerType/ownerId — just a column. Reach for the collection above instead when a user can have several images or you want lazy (on-demand) conversions. See Concepts → Attachments for the full comparison and the per-ORM column wiring.