{"id":27934,"date":"2021-05-05T17:01:34","date_gmt":"2021-05-05T17:01:34","guid":{"rendered":"http:\/\/Signed-Image-Uploading-to-Cloudinary-with-Angular-and-NestJS"},"modified":"2025-02-08T17:32:38","modified_gmt":"2025-02-09T01:32:38","slug":"signed-image-uploading-to-cloudinary-with-angular-and-nestjs","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/","title":{"rendered":"Signed Image Uploading to Cloudinary with Angular and NestJS"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>In previous posts, I explained how to upload images, and render them in the browser using a Drag &amp; Drop component written in Angular and TypeScript. That <a href=\"https:\/\/github.com\/luixaviles\/angular-upload-images-demo\">project<\/a> has considered a secure cloud storage system for the uploaded files, provided by <a href=\"https:\/\/cloudinary.com\/\">Cloudinary<\/a>.<\/p>\n<p>However, that solution didn\u2019t consider <strong>Authenticated Uploads<\/strong>, which means that every API call was <strong>unsigned<\/strong>.<\/p>\n<p>In this <em>MediaJam<\/em> we\u2019ll add an additional layer (server-side code) using <a href=\"https:\/\/nestjs.com\/\">NestJS<\/a> to provide signed uploads to the cloud.<\/p>\n<h2>Project Setup<\/h2>\n<h3>Prerequisites<\/h3>\n<p>You\u2019ll need to have installed the following tools in your local environment:<\/p>\n<ul>\n<li>The latest LTS version of <a href=\"https:\/\/nodejs.org\/en\/about\/releases\/\">Node.js<\/a> version available is recommended.<\/li>\n<li>Either NPM or Yarn as a package manager<\/li>\n<li>The <a href=\"https:\/\/docs.nestjs.com\/cli\/overview\">Nest CLI<\/a> tool(Command-Line interface for NestJS)<\/li>\n<li>The <a href=\"https:\/\/cli.angular.io\/\">Angular CLI<\/a> tool (Command-line interface for Angular)<\/li>\n<\/ul>\n<p>Also, just make sure you have your Cloudinary account ready to use (you must verify your email if it\u2019s your first time with the platform).<\/p>\n<h3>Initialize the Project<\/h3>\n<p>Let\u2019s create a brand-new project using the Nest CLI tool following this syntax.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">nest new <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">project-name<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>In this case, we\u2019ll use <code>image-server<\/code> as the name, and the project will be implemented as the backend for the existing <a href=\"https:\/\/github.com\/luixaviles\/angular-upload-images-demo\">frontend application<\/a>.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">nest <span class=\"hljs-keyword\">new<\/span> image-server\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The previous command should create a project ready-to-go, and it will install all the initial dependencies required by the project.<\/p>\n<p>Now, open the <code>image-server<\/code> project in your favorite code editor, and pay attention to the autogenerated files and directories.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">|- image-server\/\n  |- src\/\n      |- main.ts\n      |- app.module.ts\n      |- app.controller.ts\n      |- app.service.ts\n<\/code><\/span><\/pre>\n<p>By default, you can run the initial project (just to make sure it is working fine) using <code>npm run start:dev<\/code>. This command will run the project in a <em>watch mode<\/em> and it means the project will be rebuilt after every change you apply in the source code. Useful, right?<\/p>\n<h2>Implementation<\/h2>\n<h3>Installing the Cloudinary SDK<\/h3>\n<p>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 <a href=\"https:\/\/cloudinary.com\/documentation\/upload_images#uploading_with_a_direct_call_to_the_rest_api\">REST API<\/a> or using a client library for your programming language.<\/p>\n<p>In this case, we\u2019ll proceed to install the <a href=\"https:\/\/cloudinary.com\/documentation\/node_integration\">Node.js SDK<\/a> using the following command.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install cloudinary\n<\/code><\/span><\/pre>\n<p>The Cloudinary\u2019s Node.js SDK provides a simple way for image, and video uploading as we are going to see later.<\/p>\n<h3>Creating the Cloudinary Module<\/h3>\n<p>In the Angular world, we usually create a <em>module<\/em> to group components, directives, services, pipes, etc. In the same way, we can create a module to group content in a NestJS project.<\/p>\n<p>The NestJS CLI tool provides also a way to <a href=\"https:\/\/docs.nestjs.com\/cli\/overview\">generate<\/a> different files for us. Let\u2019s proceed to create a <em>module<\/em>, a <em>service<\/em>, and a <em>provider<\/em>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">nest generate <span class=\"hljs-built_in\">module<\/span> cloudinary\nnest generate service cloudinary\nnest generate provider cloudinary\/cloudinary\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Cloudinary Configuration<\/h3>\n<p>In order to use the Node.js SDK, you must configure the <code>cloud_name<\/code>, <code>api_key<\/code>, and the <code>api_secret<\/code>. These values are account-specific and can be found on the Dashboard page of your <a href=\"https:\/\/cloudinary.com\/console\">account console<\/a>.<\/p>\n<p>In other words, this is a useful way to configure the SDK globally instead of configuring each call.<\/p>\n<p>Let\u2019s open the <code>cloudinary.provider.ts<\/code> file, and add the following code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ cloudinary.provider.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { v2 <span class=\"hljs-keyword\">as<\/span> cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'cloudinary'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> CloudinaryProvider = {\n  <span class=\"hljs-attr\">provide<\/span>: <span class=\"hljs-string\">'CLOUDINARY'<\/span>,\n  <span class=\"hljs-attr\">useFactory<\/span>: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-keyword\">return<\/span> cloudinary.config({\n        <span class=\"hljs-attr\">cloud_name<\/span>: process.env.CLOUDINARY_NAME,\n        <span class=\"hljs-attr\">api_key<\/span>: process.env.CLOUDINARY_API_KEY,\n        <span class=\"hljs-attr\">api_secret<\/span>:\n          process.env.CLOUDINARY_API_SECRET,\n    });\n  },\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>The first line imports the <code>v2<\/code> of the available API and assigns an alias to it as <code>cloudinary<\/code>.<\/li>\n<li>Next, a <a href=\"https:\/\/docs.nestjs.com\/fundamentals\/custom-providers\">Custom Provider<\/a> is defined using the <code>useFactory<\/code> syntax for creating a provider dynamically.<\/li>\n<li>The factory function takes the <code>cloudinary<\/code> API and performs the configuration using environment variables, which is a suggested method for storing the keys.<\/li>\n<\/ul>\n<p>Remember to never expose your API keys in the source code!<\/p>\n<h3>The Cloudinary Response<\/h3>\n<p>One of the good things about the Node.js SDK is the addition of the response models: <code>UploadApiResponse<\/code> and <code>UploadApiErrorResponse<\/code>.<\/p>\n<p>Let\u2019s create a union type with both of them in a new file <code>cloudinary\/cloudinary-response.ts<\/code>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ cloudinary-response.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> { UploadApiErrorResponse, UploadApiResponse } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'cloudinary'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> type CloudinaryResponse = UploadApiResponse | UploadApiErrorResponse;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019ll use the union type for the service implementation in the next section.<\/p>\n<h3>The Cloudinary Service<\/h3>\n<p>As in the Angular world, we can create a <em>Service<\/em> to have an additional layer to handle the upload operation. Also, the service can be reused from different controllers if that\u2019s needed.<\/p>\n<p>Let\u2019s update the <code>cloudinary.service.ts<\/code> file, and create the <code>uploadFile<\/code> method as follows.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ cloudinary.service.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { Injectable } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@nestjs\/common'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { v2 <span class=\"hljs-keyword\">as<\/span> cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'cloudinary'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CloudinaryResponse } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/cloudinary-response'<\/span>;\n<span class=\"hljs-keyword\">const<\/span> streamifier = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'streamifier'<\/span>);\n\n@Injectable()\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CloudinaryService<\/span> <\/span>{\n  uploadFile(file: Express.Multer.File): <span class=\"hljs-built_in\">Promise<\/span>&lt;CloudinaryResponse&gt; {\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;CloudinaryResponse&gt;<span class=\"hljs-function\">(<span class=\"hljs-params\">(resolve, reject<\/span>) =&gt;<\/span> {\n      <span class=\"hljs-keyword\">const<\/span> uploadStream = cloudinary.uploader.upload_stream(\n        <span class=\"hljs-function\">(<span class=\"hljs-params\">error, result<\/span>) =&gt;<\/span> {\n          <span class=\"hljs-keyword\">if<\/span> (error) <span class=\"hljs-keyword\">return<\/span> reject(error);\n          resolve(result);\n        },\n      );\n\n      streamifier.createReadStream(file.buffer).pipe(uploadStream);\n    });\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>So what\u2019s happening in the above code snippet?<\/p>\n<ul>\n<li>The <code>uploadFile<\/code> method is ready to receive a File using the Node.js middleware for handling <code>multipart\/form-data<\/code>, which is used for uploading files. This method will return a <code>Promise<\/code> with either <code>UploadApiErrorResponse<\/code> or <code>UploadApiResponse<\/code>.<\/li>\n<li>The <code>cloudinary.uploader.upload_stream()<\/code> writes down the uploader as a stream. There are <a href=\"https:\/\/cloudinary.com\/documentation\/node_image_and_video_upload\">different options<\/a> for uploading to the cloud from Node.js or even the browser.<\/li>\n<\/ul>\n<p>There\u2019s an interesting note to take here: The <code>Express.Multer.File<\/code> is an object containing the file metadata, and it\u2019s possible to access a <code>Buffer<\/code> containing the file itself using <code>file.buffer<\/code>.<\/p>\n<p>However, before uploading this \u201cFile\u201d to the cloud, it\u2019s necessary to convert the <em>Buffer<\/em> into a <em>Readable Stream<\/em> for the SDK. There are several options to perform this operation, and this solution adds the <code>streamifier<\/code> package from <a href=\"https:\/\/www.npmjs.com\/package\/streamifier\">NPM<\/a>.<\/p>\n<p>Install the package as part of the project:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install streamifier\n<\/code><\/span><\/pre>\n<p>And then you\u2019ll be ready to convert any <code>Buffer<\/code> or <code>string<\/code> into a <code>Readable Stream<\/code> through the <code>streamifier.createReadStream()<\/code> method.<\/p>\n<p>Read more about <code>cloudinary.upload_stream<\/code> method <a href=\"https:\/\/github.com\/cloudinary\/cloudinary_npm#cloudinaryupload_stream\">here<\/a>.<\/p>\n<h3>The Cloudinary Module<\/h3>\n<p>Let\u2019s update the <code>cloudinary.module.ts<\/code> file to ensure the right configuration of the Node.js SDK, and the Service:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ cloudinary.module.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> { Module } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@nestjs\/common'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CloudinaryProvider } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/cloudinary.provider'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CloudinaryService } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/cloudinary.service'<\/span>;\n\n@Module({\n  <span class=\"hljs-attr\">providers<\/span>: &#91;CloudinaryProvider, CloudinaryService],\n  <span class=\"hljs-attr\">exports<\/span>: &#91;CloudinaryProvider, CloudinaryService]\n})\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CloudinaryModule<\/span> <\/span>{}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>As you can see, it\u2019s required to export both <code>CloudinaryProvider<\/code> and <code>CloudinaryService<\/code> from this module.<\/p>\n<h3>The Upload Controller<\/h3>\n<p>Let\u2019s work with the <code>app.controller.ts<\/code> file to configure an appropriate route before writing the <code>uploadImage<\/code> method.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ app.controller.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> {\n  Controller,\n  Post,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@nestjs\/common'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CloudinaryService } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/cloudinary\/cloudinary.service'<\/span>;\n\n@Controller(<span class=\"hljs-string\">'image'<\/span>)\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">AppController<\/span> <\/span>{\n  <span class=\"hljs-keyword\">constructor<\/span>(private readonly cloudinaryService: CloudinaryService) {}\n\n  @Post(<span class=\"hljs-string\">'upload'<\/span>)\n  uploadImage() {\n\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The <code>@Controller('image')<\/code> decorator will define the base path as <code>\/image<\/code>, and along with the <code>@Post('upload')<\/code> decorator, will define the <code>POST \/image\/upload<\/code> endpoint.<\/p>\n<p>On other hand, an instance of the <code>CloudinaryService<\/code> class is injected into the controller as a private attribute: <code>cloudinaryService<\/code>.<\/p>\n<h3>Uploading the File with NestJS<\/h3>\n<p>NestJS has implemented File Upload capabilities to make it easier for us. It provides a built-in module based on the <a href=\"https:\/\/github.com\/expressjs\/multer\">multer<\/a> middleware package for Express. And, for better type safety, you must install the typings package:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npm<\/span> <span class=\"hljs-selector-tag\">install<\/span> <span class=\"hljs-selector-tag\">--save-dev<\/span> <span class=\"hljs-keyword\">@types<\/span>\/multer\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This middleware is enabled to handle <code>multipart\/form-data<\/code>, which is primarily used for uploading files.<\/p>\n<p>It\u2019s time to implement the <code>uploadImage<\/code> method!<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ app.controller.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> {\n  Controller,\n  Post,\n  UploadedFile,\n  UseInterceptors,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@nestjs\/common'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { FileInterceptor } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@nestjs\/platform-express'<\/span>;\n<span class=\"hljs-comment\">\/\/ ... other imports<\/span>\n\n@Controller(<span class=\"hljs-string\">'image'<\/span>)\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">AppController<\/span> <\/span>{\n  <span class=\"hljs-comment\">\/\/ ... Constructor<\/span>\n\n  @Post(<span class=\"hljs-string\">'upload'<\/span>)\n  @UseInterceptors(FileInterceptor(<span class=\"hljs-string\">'file'<\/span>))\n  uploadImage(@UploadedFile() file: Express.Multer.File) {\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">this<\/span>.cloudinaryService.uploadFile(file);\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The previous method is meant to upload a single file. Then, a <code>FileInterceptor<\/code> is needed to \u201ccatch\u201d the file, and it can be extracted from the request using the <code>@UploadedFile<\/code> decorator.<\/p>\n<p>Finally, it performs a call to the <code>CloudinaryService<\/code> implemented above to delegate the upload behavior through the SDK. The service will return a <code>Promise<\/code> with either a <code>UploadApiResponse<\/code> or <code>UploadApiErrorResponse<\/code>.<\/p>\n<p>Of course, it\u2019s possible to process multiple files in a single method. This can be done using the <code>@UploadedFiles()<\/code> decorator instead:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">  <span class=\"hljs-comment\">\/\/ app.controller.ts<\/span>\n\n  @Post(<span class=\"hljs-string\">'uploads'<\/span>)\n  @UseInterceptors(FilesInterceptor(<span class=\"hljs-string\">'file&#91;]'<\/span>, <span class=\"hljs-number\">5<\/span>))\n  uploadImages(@UploadedFiles() files: Express.Multer.File&#91;]) {\n    <span class=\"hljs-comment\">\/\/... handle multiple files<\/span>\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>You can see that we use the <code>FilesInterceptor', and that it can be configured to support a maximum of <\/code>5` files to be uploaded at the same time.<\/p>\n<h3>Enable CORS in NestJS<\/h3>\n<p>As the <a href=\"https:\/\/docs.nestjs.com\/security\/cors\">documentation site<\/a> says:<\/p>\n<blockquote>\n<p>Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.<\/p>\n<\/blockquote>\n<p>You\u2019ll find the following error in the browser\u2019s console if you don\u2019t have it enabled:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">Access to XMLHttpRequest at <span class=\"hljs-string\">'http:\/\/localhost:3000\/image\/upload'<\/span> <span class=\"hljs-keyword\">from<\/span> origin <span class=\"hljs-string\">'http:\/\/localhost:4200'<\/span> has been blocked by CORS policy: No <span class=\"hljs-string\">'Access-Control-Allow-Origin'<\/span> header is present on the requested resource.\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>NestJS makes use of the Express <code>cors<\/code> package and it can be customized for you. To enable CORS, just add a call to <code>enableCors()<\/code> method as follows in the <code>main.ts<\/code> file.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ main.ts<\/span>\n<span class=\"hljs-comment\">\/\/... imports<\/span>\n\n<span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">bootstrap<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> app = <span class=\"hljs-keyword\">await<\/span> NestFactory.create(AppModule);\n  app.enableCors(); <span class=\"hljs-comment\">\/\/ &lt;- enable CORS<\/span>\n  <span class=\"hljs-keyword\">await<\/span> app.listen(<span class=\"hljs-number\">3000<\/span>);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Live Demo<\/h2>\n<p>Find the source code available in <a href=\"https:\/\/github.com\/luixaviles\/signed-image-uploading-angular-nestjs\">GitHub<\/a> both for the Backend (using NestJS) and the Frontend (using Angular).<\/p>\n<p>If you prefer, you can play around with the project in <a href=\"https:\/\/codesandbox.io\/s\/angular-upload-images-cloudinary-ylojh\">CodeSandbox &#8211; Uploading Images to Cloudinary with Angular<\/a> too, to see the image uploading in action:<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/angular-upload-images-cloudinary-ylojh?theme=dark&amp;codemirror=1&amp;highlights=&amp;editorsize=50&amp;fontsize=14&amp;expanddevtools=0&amp;hidedevtools=0&amp;eslint=0&amp;forcerefresh=0&amp;hidenavigation=0&amp;initialpath=%2F&amp;module=&amp;moduleview=0&amp;previewwindow=&amp;view=&amp;runonclick=1\"\n      height=\"500\"\n      style=\"width: 100%;\"\n      title=\"Angular Upload Images to Cloudinary\"\n      loading=\"lazy\"\n      allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n      sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n    ><\/iframe>\n  <\/div>\n\n  <div class=\"wp-block-cloudinary-markdown \"><hr \/>\n<p>Feel free to reach out on <a href=\"https:\/\/twitter.com\/luixaviles\">Twitter<\/a> if you have any questions. Follow me on <a href=\"https:\/\/github.com\/luixaviles\">GitHub<\/a> to see more about my work.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[391,134,370,371],"class_list":["post-27934","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-angular","tag-guest-post","tag-image","tag-under-review"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Signed Image Uploading to Cloudinary with Angular and NestJS<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Signed Image Uploading to Cloudinary with Angular and NestJS\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2021-05-05T17:01:34+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-02-09T01:32:38+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Signed Image Uploading to Cloudinary with Angular and NestJS\",\"datePublished\":\"2021-05-05T17:01:34+00:00\",\"dateModified\":\"2025-02-09T01:32:38+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\"},\"wordCount\":9,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"keywords\":[\"Angular\",\"Guest Post\",\"Image\",\"Under Review\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2021\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\",\"name\":\"Signed Image Uploading to Cloudinary with Angular and NestJS\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"datePublished\":\"2021-05-05T17:01:34+00:00\",\"dateModified\":\"2025-02-09T01:32:38+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Signed Image Uploading to Cloudinary with Angular and NestJS\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Signed Image Uploading to Cloudinary with Angular and NestJS","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/","og_locale":"en_US","og_type":"article","og_title":"Signed Image Uploading to Cloudinary with Angular and NestJS","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/","og_site_name":"Cloudinary Blog","article_published_time":"2021-05-05T17:01:34+00:00","article_modified_time":"2025-02-09T01:32:38+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/"},"author":{"name":"","@id":""},"headline":"Signed Image Uploading to Cloudinary with Angular and NestJS","datePublished":"2021-05-05T17:01:34+00:00","dateModified":"2025-02-09T01:32:38+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/"},"wordCount":9,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"keywords":["Angular","Guest Post","Image","Under Review"],"inLanguage":"en-US","copyrightYear":"2021","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/","name":"Signed Image Uploading to Cloudinary with Angular and NestJS","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"datePublished":"2021-05-05T17:01:34+00:00","dateModified":"2025-02-09T01:32:38+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/signed-image-uploading-to-cloudinary-with-angular-and-nestjs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Signed Image Uploading to Cloudinary with Angular and NestJS"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27934","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=27934"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27934\/revisions"}],"predecessor-version":[{"id":36754,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27934\/revisions\/36754"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=27934"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=27934"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=27934"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}