Aviary
Recipes

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

users.controller.ts
@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 },
    });
  }
}
users.service.ts
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:

users.service.ts
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.

On this page