Node.js : Upload binary image "on-the-fly" with Fastify and Cloudinary

Node.js : Upload binary image "on-the-fly" with Fastify and Cloudinary

In this article I will describe "on-the-fly" upload of a binary image, using Fastify and Cloudinary in an Node.js environnement.

Context

Fastify is a high performance web framework built for Node.js. In my opinion it is the best web framework nowadays (as I like to call it "Express 2.0") for backend purposes.

Cloudinary is a Content Delivery Network, which allow us to perform file upload and storage in a very efficient way. I like it a lot because of all the features around pictures manipulations, and because it provide very good package for free users.

The whole reason when using a CDN is to avoid storing files in your backend storage (wether it is database or file system), because it can lead to performance issues or storage overflow. This is the reason why I implemented a quick way to Upload image from a distant device to a CDN without storing any file.

My use case is written in Typescript, but the equivalent can be done with standard Javascript.

Requirements

First, you will need to install some packages

npm install fastify fastify-multer cloudinary

You will also need to create an account on Cloudinary, you can do it for free.

The core logic

Getting the data from outside

We need to initialize the fastify router, and bind it fastify-multer plugin :

import fastify from 'fastify'
import multer from 'fastify-multer'

const server = fastify()
server.register(multer.contentParser)

server.listen(8080, (err, address) => {
  if(err) {
    console.error(err)
    process.exit(1)
  }
  console.log(`Server listening at ${address}`)
})

Then, we add a new POST route, /api/profilePicture with a specific preHandler from multer package :

import fastify from 'fastify'
import multer from 'fastify-multer'
const storage = multer.memoryStorage()  
const upload = multer({ storage: storage })

... // See above

server.post('/api/profilePicture', {preHandler: upload.single('file')},  
 this.uploadProfilePicture.bind(this))

async uploadProfilePicture(req, res): Promise<string> {  
   const binaryData = req.file.buffer
   // TODO : do stuff
   return ''
}

We just set the storage mode for multer to memory storage, so it does not store the file in the file system, but as a Buffer object.

To send a picture, we consume a content of type multipart/form-data, that provides key-value couples. With the preHandler option, we specify which key provides the data to read from the form data, in our case then name of the key is file.

So, what does multer does with the uploaded data ? This is when the magic happens, multerstores the file data into req.file. We can access the content of this file with the buffer field.

There is nothing more to do for this part, we already have our file ready to upload on binaryDate variable !

Uploading the data to Cloudinary

First of all, you will need to setup your Cloudinary client using 3 credentials you can find on your account's dashboard : cloud name, api key, api secret.

cloudinary.config({  
  cloud_name : process.env.CDN_CLOUD_NAME,  
  api_key: process.env.CDN_API_KEY,  
  api_secret: process.env.CDN_API_SECRET  
})

Then, you can use your client to upload the data to Cloudinary, and retrieve the result. To do so, I like to "promisify" the function in order to ease the potential logic around.

In my example, I perform an eager transformation on my picture, allowing to have a version of the picture cropped and centered in the face of the person (for a profile picture as an example) built by Cloudinary. Cloudinary provides tons of options to deal with pictures and it is likely that you find happiness among them.

uploadPicture(content: Buffer): Promise<object> {  
    return new Promise((resolve, reject) => {  
        cloudinary.uploader.upload_stream(  
            {  
                folder: 'profile_pictures',  
                eager : [{ width : 400, height : 400, crop : 'crop', gravity : 'face'}]  
            }, (error, result) => {  
                if (error) {  
                    throw Exception('Upload failed')
                  } else {  
                    resolve(result)  
                 }  
            }  
        ).end(content)  
    })  
}

Wrapping everything up

The final route code looks like this :

import fastify from 'fastify' import multer from 'fastify-multer' const storage = multer.memoryStorage() const upload = multer({ storage: storage })
const storage = multer.memoryStorage()  
const upload = multer({ storage: storage })

const server = fastify()
server.register(multer.contentParser)

server.listen(8080, (err, address) => {
  if(err) {
    console.error(err)
    process.exit(1)
  }
  console.log(`Server listening at ${address}`)
})

server.post('/api/profilePicture', {preHandler: upload.single('file')},  
 this.uploadProfilePicture.bind(this))

async uploadProfilePicture(req, res): Promise<string> {  
   const binaryData = req.file.buffer
   const result = await uploadPicture(binaryData) // Watch below for details
   return result.url
}

In this example I only return the URL of the uploaded picture, but in a more real life use, I would store it in my database, in order to send to frontend the URL of the picture when needed.

I really liked this way of working with file uploads, and I highly recommend you to use that kind of strategy for your projects.

Thanks for reading !