Integrating Cloudinary with NestJS for Image Management

Integrating Cloudinary with NestJS for Image Management


In this article, we will explore how to integrate Cloudinary into a NestJS application for effective image management. We will begin by setting up the CloudinaryProvider and then break down the ImageMetaService into manageable fragments to better understand its functionalities.

1. Setting Up the Cloudinary Provider

The CloudinaryProvider is responsible for configuring the Cloudinary API with your credentials. This setup allows your application to interact with Cloudinary for image uploads and management.

import { Provider } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { v2 as CloudinaryAPI } from "cloudinary";

export const CLOUDINARY = "CLOUDINARY";

export const CloudinaryProvider: Provider = {
  provide: CLOUDINARY,
  useFactory: (configService: ConfigService) => {
    return CloudinaryAPI.config({
      cloud_name: configService.get<string>("CLD_CLOUD_NAME"),
      api_key: configService.get<string>("CLD_API_KEY"),
      api_secret: configService.get<string>("CLD_API_SECRET"),
    });
  },
  inject: [ConfigService], // Inject the ConfigService
};

Explanation

  • The provider uses the ConfigService to securely fetch Cloudinary credentials from the environment variables.
  • It configures Cloudinary with cloud_name, api_key, and api_secret to establish a connection with the Cloudinary service.

2. The ImageMetaService Class

Next, we define the ImageMetaService class, which will handle all image-related operations, such as uploading and deleting images.

import {
  BadRequestException,
  HttpException,
  Injectable,
  InternalServerErrorException,
  Logger,
} from "@nestjs/common";
import { ImageMetaRepository } from "./image-meta.repository";

@Injectable()
export class ImageMetaService {
  private readonly logger: Logger = new Logger(ImageMetaService.name);

  constructor(private readonly imageMetaRepository: ImageMetaRepository) {}
}

Explanation

  • The @Injectable() decorator allows the service to be injected into other components.
  • A logger instance is created for logging errors and operational messages.
  • The ImageMetaRepository is injected for interacting with the image metadata in the database.

3. Creating a Single Image

The createSingleImage method handles the uploading of a single image to Cloudinary and storing its metadata in the repository.

async createSingleImage(singleImageFile: Express.Multer.File, ownerId: string): Promise<ImageMetaDocument> {
  try {
    if (!singleImageFile) {
      throw new BadRequestException("No image file provided");
    }

    const extension = this.getFileExtension(singleImageFile.originalname);
    const uploadResult = await this.uploadImageToCloudinary(singleImageFile);

    const singleImage = await this.imageMetaRepository.create({
      url: uploadResult.secure_url,
      name: uploadResult.public_id,
      extension: extension,
      size: singleImageFile.size,
      mimeType: singleImageFile.mimetype,
      ownerId: ownerId,
    });

    return singleImage;
  } catch (error) {
    this.logger.error(`Error creating single image:`, error);
    if (error instanceof HttpException) {
      throw error;
    } else {
      throw new InternalServerErrorException("Failed to create single image");
    }
  }
}

Explanation

  • Checks if an image file is provided, throwing a BadRequestException if not.
  • Retrieves the file extension and uploads the image to Cloudinary.
  • Saves the image metadata in the repository, including the secure URL, public ID, size, and MIME type.
  • Logs errors and handles exceptions gracefully.

4. Creating Multiple Images

The createMultipleImages method allows uploading of multiple images at once.

async createMultipleImages(multipleImageFiles: Express.Multer.File[], ownerId: string): Promise<ImageMetaDocument[]> {
  try {
    if (!multipleImageFiles || multipleImageFiles.length === 0) {
      throw new BadRequestException("No image files provided");
    }

    const multipleImages = await Promise.all(
      multipleImageFiles.map(async (image) => await this.createSingleImage(image, ownerId)),
    );

    return multipleImages;
  } catch (error) {
    this.logger.error(`Error creating multiple images:`, error);
    if (error instanceof HttpException) {
      throw error;
    } else {
      throw new InternalServerErrorException("Failed to create multiple images");
    }
  }
}

Explanation

  • Validates the presence of image files and throws a BadRequestException if none are provided.
  • Uses Promise.all to concurrently process multiple image uploads, calling createSingleImage for each file.
  • Handles errors in a similar fashion to the single image creation method.

5. Removing an Image

The removeImage method is responsible for deleting an image from Cloudinary and the database.

async removeImage(imageId: string, ownerId: string): Promise<ImageMetaDocument | null> {
  try {
    const deletedImage = await this.imageMetaRepository.getOneWhere({
      _id: imageId,
      ownerId: ownerId,
    });

    if (!deletedImage) {
      throw new Error(`Could not find image with id: ${imageId}`);
    }

    await this.deleteImageFromCloudinary(deletedImage.name);
    await this.imageMetaRepository.removeOneById(imageId);

    return deletedImage;
  } catch (error) {
    if (error instanceof HttpException) throw error;

    this.logger.error(`Error deleting image:`, error);
    throw new InternalServerErrorException("Could not delete image");
  }
}

Explanation

  • Retrieves the image metadata based on the provided imageId and ownerId.
  • Throws an error if the image is not found.
  • Deletes the image from Cloudinary and the local repository, logging errors appropriately.

6. Uploading Images to Cloudinary

The uploadImageToCloudinary method manages the upload process to Cloudinary.

async uploadImageToCloudinary(file: Express.Multer.File): Promise<UploadApiResponse> {
  return new Promise<UploadApiResponse>((resolve, reject) => {
    const uploadStream = CloudinaryAPI.uploader.upload_stream(
      (error: UploadApiErrorResponse | undefined, result: UploadApiResponse | undefined) => {
        if (error) {
          this.logger.error(`Failed to upload image to Cloudinary`, error);
          reject(error);
        } else if (!result) {
          const errorMessage = "Upload result is undefined";
          this.logger.error(`Failed to upload image to Cloudinary: ${errorMessage}`);
          reject(new Error(errorMessage));
        } else {
          resolve(result);
        }
      },
    );

    const stream = toStream(file.buffer);
    stream.pipe(uploadStream);
  });
}

Explanation

  • Utilizes a stream to upload the image to Cloudinary.
  • Resolves the promise with the upload result or rejects it with an error, logging any issues encountered during the upload process.

7. Deleting Images from Cloudinary

The deleteImageFromCloudinary method facilitates the removal of images from Cloudinary.

async deleteImageFromCloudinary(publicId: string): Promise<DeleteApiResponse> {
  try {
    return await CloudinaryAPI.uploader.destroy(publicId);
  } catch (error) {
    this.logger.error(`Failed to delete image from Cloudinary with public ID: ${publicId}`, error);
    throw error;
  }
}

Explanation

  • Deletes an image using its public ID and handles any errors by logging them.

8. Utility Function for File Extension

The getFileExtension method extracts the file extension from the original filename.

private getFileExtension(originalName: string): string {
  const lastDotIndex = originalName?.lastIndexOf(".");
  return lastDotIndex === -1 ? "" : originalName?.slice(lastDotIndex + 1);
}

Explanation

  • Retrieves the last index of the dot in the filename to determine the extension, returning an empty string if no extension is found.

9. Setting Up the ImageMetaModule

The ImageMetaModule consolidates the ImageMetaService, and CloudinaryProvider, enabling their usage throughout your NestJS application.

import { Global, Module } from "@nestjs/common";
import { CloudinaryProvider } from "../../utility/provider/cloudinary.provider";
import { ImageMetaService } from "./image-meta.service";

@Global()
@Module({
  imports: [],
  providers: [ImageMetaService, CloudinaryProvider],
  exports: [ImageMetaService],
})
export class ImageMetaModule {}

Explanation

  • The @Global() decorator makes the module globally available throughout the application, which is useful for shared services.

By breaking down the CloudinaryProvider and ImageMetaService, we’ve seen how to manage image uploads and deletions with Cloudinary in a NestJS application. This modular approach allows for better readability and maintainability in your codebase.

Integrating Cloudinary into your NestJS applications not only enhances image management but also provides scalability for future needs. Happy coding!