Upload Images with Vercel Serverless Functions

Ifeoma Imoh

With serverless functions, we can write backend or server-side code without the burden of managing servers. One of the core features of Next.js is the API routes which provide an easy solution to create an API endpoint by allowing us to write server-side logic within our Next.js application, which can then be deployed as Serverless Functions to Vercel.

In this article, we'll look at the process of uploading images from a Next.js application to Cloudinary using Vercel's Serverless Functions.

Here is a link to the demo CodeSandbox.

Setting Up the Project

Create a Next.js app using the following command:

1npx create-next-app vercel-severless-upload

Next, add the project dependencies using the following command:

1npm install cloudinary axios multer streamifier dotenv

The Node Cloudinary SDK will provide easy-to-use methods to interact with the Cloudinary APIs. axios will serve as our HTTP client. Multer is a Node.js middleware used to handle multipart/form-data, the dotenv module will allow us to parse environment variables defined in a .env file, and streamifier is used to convert a Buffer/String into a readable stream.

Setup Tailwind

We will use Tailwind CSS to give our application a decent look. Run the following command in your terminal:

1npm install -D tailwindcss postcss autoprefixer
2npx tailwindcss init -p

Now add the following to your tailwind.config.js file:

1module.exports = {
2 content: [
3 "./pages/**/*.{js,ts,jsx,tsx}",
4 "./components/**/*.{js,ts,jsx,tsx}",
5 ],
6 theme: {
7 extend: {},
8 },
9 plugins: [],

Add the following to your styles/global.css file:

1@tailwind base;
2@tailwind components;
3@tailwind utilities;

Setting Up Cloudinary

First, sign up for a free Cloudinary account if you don’t have one already. Displayed on your account’s Management Console (aka Dashboard) are your Cloudinary credentials: your cloud name, API key, etc.

Next, let’s create environment variables to hold the details of our Cloudinary account. Create a new file called .env at the root of your project and add the following to it:


This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file resides in the .gitignore folder, mitigating the security risk of inadvertently exposing secret credentials to the public. You can update the .env.local file with your Cloudinary credentials.

Writing the Upload Logic

Let’s create a Serverless Function that will be used to upload images to Cloudinary. Create a file called upload.js in your pages/api directory. Next.js treats files created inside the pages/api folder as API routes, and with these API routes, we can write some server-side logic to handle HTTP requests. In our case, we will need just one route. Add the following to your upload.js file:

1import multer from "multer";
2import { v2 as cloudinary } from "cloudinary";
3import dotenv from "dotenv";
4import streamifier from "streamifier";
6const storage = multer.memoryStorage();
7const upload = multer({ storage });
8const uploadMiddleware = upload.single("file");
10 cloud_name: process.env.CLOUD_NAME,
11 api_key: process.env.API_KEY,
12 api_secret: process.env.API_SECRET,
13 secure: true,
15function runMiddleware(req, res, fn) {
16 return new Promise((resolve, reject) => {
17 fn(req, res, (result) => {
18 if (result instanceof Error) {
19 return reject(result);
20 }
21 return resolve(result);
22 });
23 });
25export default async function handler(req, res) {
26 await runMiddleware(req, res, uploadMiddleware);
27 console.log(req.file.buffer);
28 const stream = await cloudinary.uploader.upload_stream(
29 {
30 folder: "demo",
31 },
32 (error, result) => {
33 if (error) return console.error(error);
34 res.status(200).json(result);
35 }
36 );
37 streamifier.createReadStream(req.file.buffer).pipe(stream);
39export const config = {
40 api: {
41 bodyParser: false,
42 },

In the code above, we first bring the necessary imports. We set up the Multer middleware, which provides us with two storage options: disk and memory storage. To keep things simple, we created an instance of Multer and initialized it with the storage option because we want our parsed files to be stored temporarily on the RAM; its return values are stored in a variable called upload. Our application supports selecting one file, so we used Multer’s upload.single method and passed a string that will later be used to check for and parse files in the request body.

Next, we configure it with an object consisting of our Cloudinary credentials. We then define a utility function — runMiddleware, and as its name suggests, it allows us to run our middleware. The function will wait for the Multer middleware to execute before continuing or throw an error if an error occurs.

Finally, we define and export a request handler function that will be triggered by an HTTP request made to /api/upload. This function starts by parsing the file contained in the request body using the Multer middleware. Once the file is parsed successfully, the multer middleware attaches a file property to the request body, i.e., req.file, which contains information about the file.

Next, we pipe from a readable stream by converting the buffer in the parsed file to a readable stream using the streamifier library.

We are piping to the Cloudinary uploader using the upload_stream() method, which accepts two arguments. The first is an object where you can define your upload parameters (we define our destination folder — demo), and the second is a callback function that gets called when the piping is complete. The callback accepts two parameters — an error and the result. If an error occurs, it will be logged to the terminal, and if it is successful, we send the upload response we get from cloudinary back to the client-side.

By default, API Route handler functions provide us with middleware ‒ bodyParser under the hood that automatically parses the contents of the request body, cookies, and queries. We overwrite this behavior by exporting a config object to disable the built-in parsing mechanism since Multer does that for us.

Building the Frontend

We will now compose a simple user interface to communicate with the serverless function. Add the following to your index.js file:

1import { useState } from "react";
2const UploadState = {
3 IDLE: 1,
8export default function Home() {
9 const [uploadState, setUploadState] = useState(UploadState.IDLE);
10 const [imgUrl, setImgUrl] = useState("");
11 async function handleFormData(e) {
12 setUploadState(UploadState.UPLOADING);
13 const file = e.target.files[0];
14 const formData = new FormData();
15 formData.append("file", file);
16 const res = await fetch("/api/upload", {
17 method: "POST",
18 body: formData,
19 });
20 const data = await res.json();
21 setImgUrl(data.secure_url);
22 setUploadState(UploadState.UPLOADED);
23 }
24 return (
25 <div className="flex justify-center h-screen items-center">
26 {uploadState !== UploadState.UPLOADED ? (
27 <div className="w-32">
28 <label
29 htmlFor="image"
30 className="block bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 text-center"
31 >
32 {uploadState === UploadState.UPLOADING ? (
33 <span>Uploading...</span>
34 ) : (
35 <span>Upload</span>
36 )}
37 <input
38 type="file"
39 name="file"
40 id="image"
41 className="hidden"
42 onChange={handleFormData}
43 />
44 </label>
45 </div>
46 ) : (
47 <div className="w-96 text-green-500 ">
48 <span className="block py-2 px-3 text-green-500 text-center">
49 Uploaded!
50 </span>
51 <img className="w-full" src={imgUrl} alt="Uploaded image" />
52 </div>
53 )}
54 </div>
55 );

This component allows users to select an image from their computer and gives them a button to upload the image, and it displays the image after a successful upload.

We created a function called handleFormData, which embeds the file selected by the user into the request payload using the FormData API. Note that the string passed to the formData.append() method must be the same as the string we specified in our Multer instance in the API file. The payload is then used to initiate an HTTP request to the serverless function we created. It then sets the state of imgURL as well as uploadState.

In the JSX returned, we’re rendering an upload button. If an image is uploaded successfully, we render the image instead.

Now you can start your application on http://localhost:3000/ with the following command:

1npm run dev

Once the app is up and running, you should be able to select a file and upload it. Here's what mine looks like after a successful upload.

Find the complete project here on GitHub.


This post walks you through the process of uploading images to Cloudinary using Next.js and Vercel’s Serverless Functions. With Next.js API routes, we can write server-side logic within our Next.js applications, which can then be deployed as Serverless Functions to Vercel. See here for more on how to deploy a Next.js API route as a serverless function to Vercel.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.