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
, andapi_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, callingcreateSingleImage
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
andownerId
. - 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!