NestboltNestbolt

@nestbolt/medialibrary

Quick Start

Get up and running with @nestbolt/medialibrary in five minutes -- module setup, file upload, entity association, and URL retrieval.

This guide walks through a complete working example: configuring the module, uploading a file, associating it with an entity, and retrieving its URL.

1. Register the Module

Import MediaModule in your root AppModule and call forRoot() with your disk configuration. The module registers globally, so you only need to import it once.

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { MediaModule } from "@nestbolt/medialibrary";
import { Post } from "./posts/post.entity";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: "postgres",
      host: "localhost",
      port: 5432,
      database: "myapp",
      username: "postgres",
      password: "postgres",
      entities: [Post],
      synchronize: true, // development only
    }),
    MediaModule.forRoot({
      defaultDisk: "local",
      disks: {
        local: {
          driver: "local",
          root: "./uploads",
          urlBase: "/media",
        },
      },
    }),
  ],
})
export class AppModule {}

2. Create Your Entity

Decorate your entity with @HasMedia() and extend HasMediaMixin to get built-in media methods. Define a collection and an image conversion:

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import {
  HasMedia,
  HasMediaMixin,
  RegisterMediaCollections,
  RegisterMediaConversions,
} from "@nestbolt/medialibrary";

@Entity("posts")
@HasMedia()
@RegisterMediaCollections((addCollection) => {
  addCollection("images")
    .acceptsMimeTypes(["image/jpeg", "image/png", "image/webp"])
    .onlyKeepLatest(10);
})
@RegisterMediaConversions((addConversion) => {
  addConversion("thumbnail")
    .resize(300, 300, { fit: "cover" })
    .format("webp")
    .quality(80)
    .performOnCollections("images");
})
export class Post extends HasMediaMixin(BaseEntity) {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  title!: string;
}

3. Build a Service That Uploads Files

Inject MediaService and use it to upload files and query media:

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { MediaService, MediaEntity } from "@nestbolt/medialibrary";
import { Post } from "./post.entity";

@Injectable()
export class PostService {
  constructor(
    @InjectRepository(Post)
    private readonly postRepo: Repository<Post>,
    private readonly mediaService: MediaService,
  ) {}

  async createPost(title: string, imageBuffer: Buffer, fileName: string): Promise<Post> {
    const post = this.postRepo.create({ title });
    await this.postRepo.save(post);

    // Upload the image and associate it with the post
    await this.mediaService
      .addMediaFromBuffer(imageBuffer, fileName)
      .forModel("Post", post.id)
      .withCustomProperties({ alt: title })
      .toMediaCollection("images");

    return post;
  }

  async getPostImages(postId: string): Promise<MediaEntity[]> {
    return this.mediaService.getMedia("Post", postId, "images");
  }

  async getPostThumbnailUrl(postId: string): Promise<string> {
    const media = await this.mediaService.getFirstMedia("Post", postId, "images");
    if (!media) return "";
    return this.mediaService.getUrl(media, "thumbnail");
  }
}

4. Create a Controller

Wire up an HTTP endpoint that accepts file uploads:

import {
  Controller,
  Post as HttpPost,
  UploadedFile,
  UseInterceptors,
  Param,
  Get,
  Body,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { PostService } from "./post.service";

@Controller("posts")
export class PostController {
  constructor(private readonly postService: PostService) {}

  @HttpPost()
  @UseInterceptors(FileInterceptor("image"))
  async create(
    @Body("title") title: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    const post = await this.postService.createPost(
      title,
      file.buffer,
      file.originalname,
    );
    return { id: post.id, title: post.title };
  }

  @Get(":id/images")
  async getImages(@Param("id") id: string) {
    const images = await this.postService.getPostImages(id);
    return images.map((media) => ({
      id: media.id,
      fileName: media.fileName,
      size: media.humanReadableSize,
      url: this.postService.mediaService.getUrl(media),
      thumbnailUrl: this.postService.mediaService.getUrl(media, "thumbnail"),
    }));
  }
}

5. Using the Entity Mixin Directly

Because Post extends HasMediaMixin, you can also work with media directly on entity instances without injecting MediaService:

const post = await postRepo.findOneBy({ id: postId });

// Upload via the entity
await post.addMedia(imageBuffer, "photo.jpg").toMediaCollection("images");

// Query via the entity
const images = await post.getMedia("images");
const firstImage = await post.getFirstMedia("images");
const thumbnailUrl = await post.getFirstMediaUrl("images", "thumbnail");
const hasImages = await post.hasMedia("images");

// Clean up
await post.clearMediaCollection("images");

6. Test It

Upload a file using curl:

curl -X POST http://localhost:3000/posts \
  -F "title=My First Post" \
  -F "image=@/path/to/photo.jpg"

Then retrieve the images:

curl http://localhost:3000/posts/<post-id>/images

The response includes URLs for both the original image and the auto-generated thumbnail conversion:

[
  {
    "id": "a1b2c3d4-...",
    "fileName": "photo.jpg",
    "size": "1.2 MB",
    "url": "/media/a1b2c3d4-.../photo.jpg",
    "thumbnailUrl": "/media/a1b2c3d4-.../conversions/thumbnail-photo.webp"
  }
]

What Happened Behind the Scenes

  1. addMediaFromBuffer() created a FileAdder instance with the buffer and file name.
  2. .forModel("Post", post.id) associated the upload with your Post entity.
  3. .toMediaCollection("images") triggered the upload pipeline:
    • The file was validated against the "images" collection rules (MIME type check, size limit enforcement of 10 latest items).
    • A MediaEntity record was saved to the database.
    • The file was written to the configured disk (./uploads/<media-id>/photo.jpg).
    • The "thumbnail" conversion was detected (it targets the "images" collection), so Sharp resized the image to 300x300, converted it to WebP at quality 80, and saved it as ./uploads/<media-id>/conversions/thumbnail-photo.webp.
    • A media.added event was emitted (if @nestjs/event-emitter is installed).

Next Steps