Signed Image Uploading to Cloudinary with Angular and NestJS

Luis Aviles

In previous posts, I explained how to upload images, and render them in the browser using a Drag & Drop component written in Angular and TypeScript. That project has considered a secure cloud storage system for the uploaded files, provided by Cloudinary.

However, that solution didn't consider Authenticated Uploads, which means that every API call was unsigned.

In this MediaJam we'll add an additional layer (server-side code) using NestJS to provide signed uploads to the cloud.

Project Setup

Prerequisites

You'll need to have installed the following tools in your local environment:

  • The latest LTS version of Node.js version available is recommended.
  • Either NPM or Yarn as a package manager
  • The Nest CLI tool(Command-Line interface for NestJS)
  • The Angular CLI tool (Command-line interface for Angular)

Also, just make sure you have your Cloudinary account ready to use (you must verify your email if it's your first time with the platform).

Initialize the Project

Let's create a brand-new project using the Nest CLI tool following this syntax.

1nest new <project-name>

In this case, we'll use image-server as the name, and the project will be implemented as the backend for the existing frontend application.

1nest new image-server

The previous command should create a project ready-to-go, and it will install all the initial dependencies required by the project.

Now, open the image-server project in your favorite code editor, and pay attention to the autogenerated files and directories.

1|- image-server/
2 |- src/
3 |- main.ts
4 |- app.module.ts
5 |- app.controller.ts
6 |- app.service.ts

By default, you can run the initial project (just to make sure it is working fine) using npm run start:dev. This command will run the project in a watch mode and it means the project will be rebuilt after every change you apply in the source code. Useful, right?

Implementation

Installing the Cloudinary SDK

The Cloudinary platform provides a variety of options for customizing how the files can be uploaded from your application. For example, you can upload the files using the REST API or using a client library for your programming language.

In this case, we'll proceed to install the Node.js SDK using the following command.

1npm install cloudinary

The Cloudinary's Node.js SDK provides a simple way for image, and video uploading as we are going to see later.

Creating the Cloudinary Module

In the Angular world, we usually create a module to group components, directives, services, pipes, etc. In the same way, we can create a module to group content in a NestJS project.

The NestJS CLI tool provides also a way to generate different files for us. Let's proceed to create a module, a service, and a provider:

1nest generate module cloudinary
2nest generate service cloudinary
3nest generate provider cloudinary/cloudinary

Cloudinary Configuration

In order to use the Node.js SDK, you must configure the cloud_name, api_key, and the api_secret. These values are account-specific and can be found on the Dashboard page of your account console.

In other words, this is a useful way to configure the SDK globally instead of configuring each call.

Let's open the cloudinary.provider.ts file, and add the following code.

1// cloudinary.provider.ts
2
3import { v2 as cloudinary } from 'cloudinary';
4
5export const CloudinaryProvider = {
6 provide: 'CLOUDINARY',
7 useFactory: () => {
8 return cloudinary.config({
9 cloud_name: process.env.CLOUDINARY_NAME,
10 api_key: process.env.CLOUDINARY_API_KEY,
11 api_secret:
12 process.env.CLOUDINARY_API_SECRET,
13 });
14 },
15};
  • The first line imports the v2 of the available API and assigns an alias to it as cloudinary.
  • Next, a Custom Provider is defined using the useFactory syntax for creating a provider dynamically.
  • The factory function takes the cloudinary API and performs the configuration using environment variables, which is a suggested method for storing the keys.

Remember to never expose your API keys in the source code!

The Cloudinary Response

One of the good things about the Node.js SDK is the addition of the response models: UploadApiResponse and UploadApiErrorResponse.

Let's create a union type with both of them in a new file cloudinary/cloudinary-response.ts:

1// cloudinary-response.ts
2import { UploadApiErrorResponse, UploadApiResponse } from 'cloudinary';
3
4export type CloudinaryResponse = UploadApiResponse | UploadApiErrorResponse;

We'll use the union type for the service implementation in the next section.

The Cloudinary Service

As in the Angular world, we can create a Service to have an additional layer to handle the upload operation. Also, the service can be reused from different controllers if that's needed.

Let's update the cloudinary.service.ts file, and create the uploadFile method as follows.

1// cloudinary.service.ts
2
3import { Injectable } from '@nestjs/common';
4import { v2 as cloudinary } from 'cloudinary';
5import { CloudinaryResponse } from './cloudinary-response';
6const streamifier = require('streamifier');
7
8@Injectable()
9export class CloudinaryService {
10 uploadFile(file: Express.Multer.File): Promise<CloudinaryResponse> {
11 return new Promise<CloudinaryResponse>((resolve, reject) => {
12 const uploadStream = cloudinary.uploader.upload_stream(
13 (error, result) => {
14 if (error) return reject(error);
15 resolve(result);
16 },
17 );
18
19 streamifier.createReadStream(file.buffer).pipe(uploadStream);
20 });
21 }
22}

So what's happening in the above code snippet?

  • The uploadFile method is ready to receive a File using the Node.js middleware for handling multipart/form-data, which is used for uploading files. This method will return a Promise with either UploadApiErrorResponse or UploadApiResponse.
  • The cloudinary.uploader.upload_stream() writes down the uploader as a stream. There are different options for uploading to the cloud from Node.js or even the browser.

There's an interesting note to take here: The Express.Multer.File is an object containing the file metadata, and it's possible to access a Buffer containing the file itself using file.buffer.

However, before uploading this "File" to the cloud, it's necessary to convert the Buffer into a Readable Stream for the SDK. There are several options to perform this operation, and this solution adds the streamifier package from NPM.

Install the package as part of the project:

1npm install streamifier

And then you'll be ready to convert any Buffer or string into a Readable Stream through the streamifier.createReadStream() method.

Read more about cloudinary.upload_stream method here.

The Cloudinary Module

Let's update the cloudinary.module.ts file to ensure the right configuration of the Node.js SDK, and the Service:

1// cloudinary.module.ts
2import { Module } from '@nestjs/common';
3import { CloudinaryProvider } from './cloudinary.provider';
4import { CloudinaryService } from './cloudinary.service';
5
6@Module({
7 providers: [CloudinaryProvider, CloudinaryService],
8 exports: [CloudinaryProvider, CloudinaryService]
9})
10export class CloudinaryModule {}

As you can see, it's required to export both CloudinaryProvider and CloudinaryService from this module.

The Upload Controller

Let's work with the app.controller.ts file to configure an appropriate route before writing the uploadImage method.

1// app.controller.ts
2import {
3 Controller,
4 Post,
5} from '@nestjs/common';
6import { CloudinaryService } from './cloudinary/cloudinary.service';
7
8@Controller('image')
9export class AppController {
10 constructor(private readonly cloudinaryService: CloudinaryService) {}
11
12 @Post('upload')
13 uploadImage() {
14
15 }
16}

The @Controller('image') decorator will define the base path as /image, and along with the @Post('upload') decorator, will define the POST /image/upload endpoint.

On other hand, an instance of the CloudinaryService class is injected into the controller as a private attribute: cloudinaryService.

Uploading the File with NestJS

NestJS has implemented File Upload capabilities to make it easier for us. It provides a built-in module based on the multer middleware package for Express. And, for better type safety, you must install the typings package:

1npm install --save-dev @types/multer

This middleware is enabled to handle multipart/form-data, which is primarily used for uploading files.

It's time to implement the uploadImage method!

1// app.controller.ts
2
3import {
4 Controller,
5 Post,
6 UploadedFile,
7 UseInterceptors,
8} from '@nestjs/common';
9import { FileInterceptor } from '@nestjs/platform-express';
10// ... other imports
11
12@Controller('image')
13export class AppController {
14 // ... Constructor
15
16 @Post('upload')
17 @UseInterceptors(FileInterceptor('file'))
18 uploadImage(@UploadedFile() file: Express.Multer.File) {
19 return this.cloudinaryService.uploadFile(file);
20 }
21}

The previous method is meant to upload a single file. Then, a FileInterceptor is needed to "catch" the file, and it can be extracted from the request using the @UploadedFile decorator.

Finally, it performs a call to the CloudinaryService implemented above to delegate the upload behavior through the SDK. The service will return a Promise with either a UploadApiResponse or UploadApiErrorResponse.

Of course, it's possible to process multiple files in a single method. This can be done using the @UploadedFiles() decorator instead:

1// app.controller.ts
2
3 @Post('uploads')
4 @UseInterceptors(FilesInterceptor('file[]', 5))
5 uploadImages(@UploadedFiles() files: Express.Multer.File[]) {
6 //... handle multiple files
7 }

You can see that we use the FilesInterceptor', and that it can be configured to support a maximum of 5` files to be uploaded at the same time.

Enable CORS in NestJS

As the documentation site says:

Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.

You'll find the following error in the browser's console if you don't have it enabled:

1Access to XMLHttpRequest at 'http://localhost:3000/image/upload' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

NestJS makes use of the Express cors package and it can be customized for you. To enable CORS, just add a call to enableCors() method as follows in the main.ts file.

1// main.ts
2//... imports
3
4async function bootstrap() {
5 const app = await NestFactory.create(AppModule);
6 app.enableCors(); // <- enable CORS
7 await app.listen(3000);
8}

Live Demo

Find the source code available in GitHub both for the Backend (using NestJS) and the Frontend (using Angular).

If you prefer, you can play around with the project in CodeSandbox - Uploading Images to Cloudinary with Angular too, to see the image uploading in action:


Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

Luis Aviles

Senior Software Engineer

Luis is a Senior Software Engineer and Google Developer Expert in Web Technologies and Angular. He is an author of online courses, technical articles, and a public speaker. He has participated in different international technology conferences, giving technical talks, workshops, and training sessions. He’s passionate about the developer community and he loves to help junior developers and professionals to improve their skills.
When he’s not coding, Luis is doing photography or Astrophotography.