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 HERE2 API_KEY = YOUR API API KEY3 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 <input9 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 file23 </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 <FileUpload29 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");67cloudinary.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});1314function 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}2425export 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}4546export 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 port2 NEXT_PUBLIC_URL=http://localhost:300034 CLOUD_NAME = YOUR CLOUD NAME HERE5 API_KEY = YOUR API API KEY6 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";23export default function FileLinkPreview({ file, id, setStatus }) {4 const url = `${process.env.NEXT_PUBLIC_URL}/fileDownload?id=${id}`;56 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 FileLinkPreview3import FileLinkPreview from "../components/FileLinkPreview";45export default function Home() {6 const [file, setFile] = useState();7 const [status, setStatus] = useState();8 const [fileId, setFileId] = useState();910 const handleUpload = (e) => {11 //...12 };1314 const uploadFile = async () => {15 //...16 };1718 return (19 <div className={styles.app}>20 <h1>Want to share a file?</h1>21 {/* Add this */}22 {status !== "Upload successful" ? (23 <FileUpload24 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");67cloudinary.config({8 //...9});1011function runMiddleware(req, res, fn) {12 //...13}1415export 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 }2627 // Add this28 if (req.method === "GET") {29 try {30 response = await cloudinary.search31 .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}4142export 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";56export default function FileDownload() {7 const [fileResponse, setFileResponse] = useState();8 const [axiosCallStatus, setAxiosCallStatus] = useState();910 const { id } = useRouter().query;1112 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]);2627 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 <a35 href={fileResponse.secure_url.replace(36 "/upload/",37 `/upload/fl_attachment:${38 fileResponse.context.filename.split(".")[0]39 }/`40 )}41 >42 Click to download43 </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.