Have you ever found yourself in the middle of a project and realized that the tool you need to use is not supported by your framework of choice? That's exactly what happened to me recently when I was working on a NestJS application and wanted to integrate Cloudinary for image hosting.

NestJS is a powerful framework for building server-side applications with Node.js, and it offers a variety of built-in modules and middleware for common tasks such as logging, validation, and routing. However, when it comes to working with third-party services, there may be times when you need to add a custom solution to the mix.

In this blog post, I'll share my experience of serving an unsupported third-party middleware (Cloudinary) to the NestJS dependency injection layer. I'll explain the options I considered and the approach I ultimately took to solve this problem. I hope that by reading this post, you'll be able to apply these techniques to your own NestJS projects and take your skills to the next level.

Dependency injection

"Dependency injection" is a design pattern that helps to decouple parts of a system and make it more flexible and easier to test. In NestJS, the dependency injection system is based on the inversion of control (IoC) principle, which means that the framework is responsible for creating and supplying the dependencies required by a module or component.

To use dependency injection in NestJS, you first need to define a provider, which is a class or a function that returns an object or a value. This provider can then be injected into a module, controller, or service using the @Injectable() decorator.

For example, let's say you have a LoggerService that you want to use in multiple places throughout your application. You can define the LoggerService as a provider and then inject it wherever it is needed by using the @Inject() decorator:

@Injectable()
export class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

@Controller()
export class SomeController {
  constructor(@Inject(LoggerService) private logger: LoggerService) {}

  @Get()
  doSomething() {
    this.logger.log('Doing something...');
  }
}

Adding third-party middleware to NestJS

In NestJS, adding third-party middleware to your application is typically a straightforward process. First, you need to install the npm package for the middleware you want to use. Then, you can apply the middleware to a specific route or to the entire application using the @UseMiddleware() decorator.

For example, let's say you want to add the cors middleware to your NestJS application to enable cross-origin resource sharing (CORS). You can install the cors package using npm or yarn:

npm install cors

Then, you can apply the cors middleware to a specific route by using the @UseMiddleware() decorator:

import { Controller, Get, UseMiddleware } from '@nestjs/common';
import * as cors from 'cors';

@Controller()
export class SomeController {
  @Get()
  @UseMiddleware(cors())
  doSomething() {
    // ...
  }
}

Or, you can apply the cors middleware to the entire application by using the app.use() method in the root module:

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import * as cors from 'cors';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer): void {
    consumer.apply(cors()).forRoutes('*');
  }
}

This is the normal process for adding third-party middleware to NestJS, but what do you do when the middleware you want to use is not officially supported by the framework? That's the topic of the next section, where we'll discuss the options for serving unsupported third-party middleware to the NestJS dependency injection layer.

Serving unsupported third-party middleware

As mentioned earlier, I recently ran into the challenge of integrating Cloudinary in a NestJS codebase. Cloudinary is a popular cloud-based image hosting and manipulation service, but it is not officially supported by NestJS. This meant that I had to find a way to serve the Cloudinary middleware to the NestJS dependency injection layer.

After researching different options, I decided to create a custom provider that wrapped the Cloudinary middleware and made it available for injection. This approach involved creating a class or function that returned the middleware as an object or a function and then decorating it with the @Injectable() decorator. Here is an example of the CloudinaryMiddleware provider I created:

import { Injectable } from '@nestjs/common';
import * as cloudinary from 'cloudinary';

@Injectable()
export class CloudinaryMiddleware {
  getMiddleware() {
    return cloudinary.v2.uploader.upload;
  }
}

The cloudinary.v2.uploader.upload function is the main method for uploading images to Cloudinary. By exposing it as a middleware function, I was able to use the @UseMiddleware() decorator to apply it to a specific route in my NestJS application:

import { Controller, Get, Inject, UseMiddleware } from '@nestjs/common';
import { CloudinaryMiddleware } from './cloudinary.middleware';

@Controller()
export class SomeController {
  constructor(@Inject(CloudinaryMiddleware) private cloudinaryMiddleware: CloudinaryMiddleware) {}

  @Get()
  @UseMiddleware(this.cloudinaryMiddleware.getMiddleware())
  doSomething() {
    // ...
  }
}

The CloudinaryMiddleware provider is just a wrapper around the cloudinary.v2.uploader.upload function, which is the main method for uploading images to Cloudinary.

To actually use this middleware to upload files, you would need to do the following:

  1. Install the Cloudinary npm package: npm install cloudinary
  2. Set up your Cloudinary account and obtain your API key, API secret, and cloud name. You can find these details in the Cloudinary dashboard.
  3. Configure the Cloudinary npm package with your API key, API secret, and cloud name:
import * as cloudinary from 'cloudinary';

cloudinary.v2.config({
  api_key: 'YOUR_API_KEY',
  api_secret: 'YOUR_API_SECRET',
  cloud_name: 'YOUR_CLOUD_NAME'
});

4. Use the @UseMiddleware() decorator to apply the CloudinaryMiddleware middleware to a specific route or controller in your NestJS application. You can then call the cloudinary.v2.uploader.upload function within that route or controller to upload an image to Cloudinary.

Here is an example of how you might use the CloudinaryMiddleware middleware to upload an image from a NestJS route:

import { Controller, Get, Inject, UseMiddleware } from '@nestjs/common';
import { CloudinaryMiddleware } from './cloudinary.middleware';

@Controller()
export class SomeController {
  constructor(@Inject(CloudinaryMiddleware) private cloudinaryMiddleware: CloudinaryMiddleware) {}

  @Get()
  @UseMiddleware(this.cloudinaryMiddleware.getMiddleware())
  async doSomething() {
    const imagePath = '/path/to/image.jpg';
    const result = await this.cloudinaryMiddleware.getMiddleware()(imagePath);
    console.log(result); // logs the uploaded image details
  }
}

Conclusion

In this blog post, we looked at the problem of serving an unsupported third-party middleware (Cloudinary) to the NestJS dependency injection layer. We discussed two options for solving this problem: creating a custom provider, and extending the HttpAdapterHost class.

Using a custom provider, we were able to wrap the Cloudinary middleware and make it available for injection into a module or controller. This allowed us to use the @UseMiddleware() decorator to apply the Cloudinary middleware to a specific route or controller in our NestJS application.

Alternatively, we could have extended the HttpAdapterHost class and overridden the register() method to apply the Cloudinary middleware to the underlying HTTP server instance. This approach may be useful if you want to apply the middleware globally to the entire application.

I hope that by reading this blog post, you've gained a better understanding of how to serve unsupported third-party middleware to the NestJS dependency injection layer. Whether you're working with Cloudinary or another service, these techniques can help you extend the capabilities of your NestJS applications and take your skills to the next level.


References: