Build a File-Sharing Application with Cloudinary

Ifeoma Imoh

In this article, we’ll build a simple file-sharing application that allows users to upload files and generate custom download links that can then be shared. We will also see how to use the Multer middleware to parse files from the client-side to an API route in a Next.js application.

Here is a link to the demo CodeSandbox.

Project Setup

Create a new Next.js application using the following command:

1npx create-next-app file-sharing-app

Next, add the project dependencies using the following command:

1npm install cloudinary multer axios

The Cloudinary Node SDK will provide easy-to-use methods to interact with the Cloudinary APIs, Multer, to parse files coming from the client, while axios will serve as our HTTP client.

Cloudinary Setup

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 important details: 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:

1CLOUD_NAME = YOUR CLOUD NAME HERE
2 API_KEY = YOUR API API KEY
3 API_SECRET = YOUR API API SECRET

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.

Handle File Selection on the Frontend

Let's create a simple user interface that, with the click of a button, allows users to select a file of their choice.

We'll also need some styles to give the application a nice appearance. Copy the styles in this codeSandbox link to your styles/Home.module.css file.

Create a folder named components at the root level of your application. Create a file named FileUpload.js inside the components folder and add the following to it:

1import { useRef } from "react";
2import styles from "../styles/Home.module.css";
3export default function FileUpload({ file, setFile, handleUpload, status }) {
4 const fileRef = useRef();
5 return (
6 <form className={styles.form} onSubmit={handleUpload}>
7 <div className={styles.upload} onClick={() => fileRef.current.click()}>
8 <input
9 type="file"
10 ref={fileRef}
11 style={{ display: "none" }}
12 onChange={(e) => setFile(e.target.files[0])}
13 />
14 <p>Click to select a file</p>
15 </div>
16 {file && (
17 <>
18 <div>
19 <h5>{file.name}</h5>
20 </div>
21 <button type="submit" disabled={status === "Uploading..."}>
22 Upload file
23 </button>
24 <p>{status}</p>
25 </>
26 )}
27 </form>
28 );
29}

The FileUpload component expects a couple of props. The file prop is a state variable to hold the selected file. The setFile prop is a function that sets the file state. The handleUpload prop will trigger the request to an API route that handles the file upload to Cloudinary, and the status prop will keep track of the request state.

The component then returns a form with a file input field that is opened programmatically using an instance of the useRef hook. The selected file is then added to the file state, after which it renders the file name and a button that triggers submission.

To connect the FileUpload component to the main application and pass it the required props, open the pages/index.js file and replace its content with the following:

1import { useState } from "react";
2import axios from "axios";
3import FileUpload from "../components/FileUpload";
4import styles from "../styles/Home.module.css";
5export default function Home() {
6 const [file, setFile] = useState();
7 const [status, setStatus] = useState();
8 const [fileId, setFileId] = useState();
9 const handleUpload = (e) => {
10 e.preventDefault();
11 uploadFile();
12 };
13 const uploadFile = async () => {
14 setStatus("Uploading...");
15 const formData = new FormData();
16 formData.append("file", file);
17 try {
18 const response = await axios.post("/api/upload", formData);
19 setFileId(response.data.public_id);
20 setStatus("Upload successful");
21 } catch (error) {
22 setStatus("Upload failed..");
23 }
24 };
25 return (
26 <div className={styles.app}>
27 <h1>Want to share a file?</h1>
28 <FileUpload
29 file={file}
30 setFile={setFile}
31 handleUpload={handleUpload}
32 status={status}
33 />
34 </div>
35 );
36}

We defined three states to hold the selected file, the request status, and the id returned in the response data.

We defined a function named handleUpload, which gets added as a prop to the FileUpload component. The function calls an asynchronous uploadFile function when the form returned in the FileUpload gets submitted.

The uploadFile function instantiates a FormData and appends the selected file to it, which is then added to the body of the request made to an API route we’ll be working on in the next section.

Now, save the changes and start your application on http://localhost:3000 using the following command:

1npm start

You should be able to select a file with the name displayed on the screen.

Upload Selected File to Cloudinary

To create the API route referenced in the previous section that receives a file selected by the user, uploads it to Cloudinary, and sends the response back to the client. Create a file named upload.js in the pages/api folder and add the following to it:

1import multer from "multer";
2const cloudinary = require("cloudinary").v2;
3const storage = multer.memoryStorage();
4const upload = multer({ storage });
5const myUploadMiddleware = upload.single("file");
6
7cloudinary.config({
8 cloud_name: process.env.CLOUD_NAME,
9 api_key: process.env.API_KEY,
10 api_secret: process.env.API_SECRET,
11 secure: true,
12});
13
14function runMiddleware(req, res, fn) {
15 return new Promise((resolve, reject) => {
16 fn(req, res, (result) => {
17 if (result instanceof Error) {
18 return reject(result);
19 }
20 return resolve(result);
21 });
22 });
23}
24
25export default async function handler(req, res) {
26 let response;
27 if (req.method === "POST") {
28 await runMiddleware(req, res, myUploadMiddleware);
29 const file = req.file;
30 try {
31 const b64 = Buffer.from(file.buffer).toString("base64");
32 let dataURI = "data:" + file.mimetype + ";base64," + b64;
33 response = await cloudinary.uploader.upload(dataURI, {
34 folder: "file-sharing-app",
35 resource_type: "auto",
36 context: `filename=${file.originalname}`,
37 });
38 } catch (error) {
39 res.status(400).json(error);
40 return;
41 }
42 }
43 return res.status(200).json(response);
44}
45
46export const config = {
47 api: {
48 bodyParser: false,
49 },
50};

We are importing the Multer middleware and the v2 instance of Cloudinary. Next, we set up the multer middleware. Multer provides two types of storage: disk and memory storage. To keep things simple, we select memory storage. We then called the Multer instance and passed it the storage option as an argument with its return values saved in a variable called upload.

Because we only want to upload one file at a time in our application, we used Multer's upload.single method and passed it a string that would later be used to check for and parse files in the request body.

We also configured our Cloudinary instance with our Credentials. Next, we added a utility function — runMiddleware, and as its name suggests, it allows us to run our middleware. The function returns a promise that resolves when the middleware callback passed to it runs successfully or rejects when there is an error.

Finally, we defined our route handler. It checks if the request coming in is a POST request, after which we run our Multer middleware by passing it to the runMiddleware utility function together with the req and res objects. The middleware parses the request and appends a file attribute to the req object.

We also constructed a data URI that holds the base64 encoded data representing the selected file, which is then further uploaded to a folder called file-sharing-app in your Cloudinary account.

For the upload, we added a context parameter that sets a key-value pair of custom contextual metadata (which will be used in a later section), which will be attached to the uploaded file. We set the value of resource_type to auto so it automatically detects the file type.

We also export a config object in the file to disable the built-in body parser since we are using Multer.

Generating a Download Link

We've been able to set up the file upload aspect of our application, both the UI and the API route. Next, when a file is uploaded, we want to render a new component that displays a custom download link that can be shared.

Open the .env.local file and update its content as shown below:

1# change the 3000 if you're on a different port
2 NEXT_PUBLIC_URL=http://localhost:3000
3
4 CLOUD_NAME = YOUR CLOUD NAME HERE
5 API_KEY = YOUR API API KEY
6 API_SECRET = YOUR API API SECRET

We added a new environment variable to define the application’s host URL. Since we are in a development environment, the default is http://localhost:3000. However, that has to be changed to your preferred host URL in production.

Next, create a new file called FileLinkPreview.js in the components folder and add the following to it:

1import styles from "../styles/Home.module.css";
2
3export default function FileLinkPreview({ file, id, setStatus }) {
4 const url = `${process.env.NEXT_PUBLIC_URL}/fileDownload?id=${id}`;
5
6 const handleCopy = async () => {
7 await window.navigator.clipboard.writeText(url);
8 };
9 return (
10 <div className={styles.box}>
11 <h4>{file.name}</h4>
12 <p>File uploaded to Cloudinary, share this link to others.</p>
13 <p>{url}</p>
14 <button className={styles.copy} onClick={handleCopy}>
15 Copy!
16 </button>
17 <button onClick={() => setStatus("")}>Upload new file</button>
18 </div>
19 );
20}

The FileLinkPreview component accepts as props a file, its Cloudinary generated id, and a function to set the request status. Next, we constructed a custom link that gets rendered to the screen. We also added a button that triggers the handleCopy function that copies the custom URL to the clipboard.

Now update your pages/index.js file with the following:

1//...
2//import FileLinkPreview
3import FileLinkPreview from "../components/FileLinkPreview";
4
5export default function Home() {
6 const [file, setFile] = useState();
7 const [status, setStatus] = useState();
8 const [fileId, setFileId] = useState();
9
10 const handleUpload = (e) => {
11 //...
12 };
13
14 const uploadFile = async () => {
15 //...
16 };
17
18 return (
19 <div className={styles.app}>
20 <h1>Want to share a file?</h1>
21 {/* Add this */}
22 {status !== "Upload successful" ? (
23 <FileUpload
24 file={file}
25 setFile={setFile}
26 handleUpload={handleUpload}
27 status={status}
28 />
29 ) : (
30 <FileLinkPreview file={file} id={fileId} setStatus={setStatus} />
31 )}
32 </div>
33 );
34}

In the updated index.js file, we import the FileLinkPreview component and render it only if the file upload is successful.

Save the changes, and preview the application in your browser.

Fetch File by ID

In this section, we'll set up our API route so that once a file is uploaded to Cloudinary, we can retrieve that file by its id once a GET request to the API route with the file id is made. Open the pages/api/upload.js file and update it with the following:

1import multer from "multer";
2const cloudinary = require("cloudinary").v2;
3const storage = multer.memoryStorage();
4const upload = multer({ storage });
5const myUploadMiddleware = upload.single("file");
6
7cloudinary.config({
8 //...
9});
10
11function runMiddleware(req, res, fn) {
12 //...
13}
14
15export default async function handler(req, res) {
16 let response;
17 if (req.method === "POST") {
18 await runMiddleware(req, res, myUploadMiddleware);
19 const file = req.file;
20 try {
21 //...
22 } catch (error) {
23 //...
24 }
25 }
26
27 // Add this
28 if (req.method === "GET") {
29 try {
30 response = await cloudinary.search
31 .expression(`public_id=${req.query.id}`)
32 .with_field("context")
33 .execute();
34 } catch (error) {
35 res.status(400).json(error);
36 return;
37 }
38 }
39 return res.status(200).json(response);
40}
41
42export const config = {
43 //...
44};

We added a new conditional statement that checks if a request made to the API route is a GET request. If it is, we use the Cloudinary search API and its method parameters to retrieve the resource. We included an expression parameter to return any resource with the specified id, chained with a with_field parameter to include any context metadata defined during upload (remember, we added a context during upload).

Create a Download Page

Let's create a separate page route that matches the path defined in the custom download link generated by the application. When loaded, the page will initiate a GET request to our API route to get the file with the ID specified and make it available for download.

Create a file named fileDownload.js in the pages folder and add the following to it:

1import { useEffect, useState } from "react";
2import { useRouter } from "next/router";
3import axios from "axios";
4import styles from "../styles/Home.module.css";
5
6export default function FileDownload() {
7 const [fileResponse, setFileResponse] = useState();
8 const [axiosCallStatus, setAxiosCallStatus] = useState();
9
10 const { id } = useRouter().query;
11
12 useEffect(() => {
13 if (id === undefined) return;
14 (async () => {
15 setAxiosCallStatus("Loading...");
16 try {
17 const response = await axios.get(`/api/upload?id=${id}`);
18 console.log(response.data.resources[0]);
19 setFileResponse(response.data.resources[0]);
20 setAxiosCallStatus("");
21 } catch (error) {
22 setAxiosCallStatus("File not found, refresh your browser");
23 }
24 })();
25 }, [id]);
26
27 return (
28 <div className={styles.app}>
29 <div className={styles.box}>
30 <h2>Your file is Ready</h2>
31 {fileResponse && fileResponse.length !== 0 ? (
32 <>
33 <h4>{fileResponse.context.filename}</h4>
34 <a
35 href={fileResponse.secure_url.replace(
36 "/upload/",
37 `/upload/fl_attachment:${
38 fileResponse.context.filename.split(".")[0]
39 }/`
40 )}
41 >
42 Click to download
43 </a>
44 </>
45 ) : (
46 <p>{axiosCallStatus || "No file found"}</p>
47 )}
48 </div>
49 </div>
50 );
51}

In the code above, we defined two states to hold the response data received from the GET request made to our API route and the request status, respectively.

Next, we extracted the id from the page route query object, and attached it as a query parameter to the request API route, after which we updated the states accordingly.

The page then renders an <a> tag that links to a transformed version of the returned secure file link. For the link, we used the fl_attachment flag from Cloudinary’s transformation URL API to alter the regular delivery URL behavior, causing the URL link to download the file as an attachment rather than embedding it in our application.

Save the changes and test the application in your browser.

You can find the complete project here on GitHub.

Resources you may find helpful

Ifeoma Imoh

Software Developer

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