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.
Create a new Next.js application using the following command:
npx create-next-app file-sharing-app
Next, add the project dependencies using the following command:
npm 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.
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:
CLOUD_NAME = YOUR CLOUD NAME HERE
API_KEY = YOUR API API KEY
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:
cp .env .env.local
Code language: CSS (css)
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.
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:
import { useRef } from "react";
import styles from "../styles/Home.module.css";
export default function FileUpload({ file, setFile, handleUpload, status }) {
const fileRef = useRef();
return (
<form className={styles.form} onSubmit={handleUpload}>
<div className={styles.upload} onClick={() => fileRef.current.click()}>
<input
type="file"
ref={fileRef}
style={{ display: "none" }}
onChange={(e) => setFile(e.target.files[0])}
/>
<p>Click to select a file</p>
</div>
{file && (
<>
<div>
<h5>{file.name}</h5>
</div>
<button type="submit" disabled={status === "Uploading..."}>
Upload file
</button>
<p>{status}</p>
</>
)}
</form>
);
}
Code language: HTML, XML (xml)
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:
import { useState } from "react";
import axios from "axios";
import FileUpload from "../components/FileUpload";
import styles from "../styles/Home.module.css";
export default function Home() {
const [file, setFile] = useState();
const [status, setStatus] = useState();
const [fileId, setFileId] = useState();
const handleUpload = (e) => {
e.preventDefault();
uploadFile();
};
const uploadFile = async () => {
setStatus("Uploading...");
const formData = new FormData();
formData.append("file", file);
try {
const response = await axios.post("/api/upload", formData);
setFileId(response.data.public_id);
setStatus("Upload successful");
} catch (error) {
setStatus("Upload failed..");
}
};
return (
<div className={styles.app}>
<h1>Want to share a file?</h1>
<FileUpload
file={file}
setFile={setFile}
handleUpload={handleUpload}
status={status}
/>
</div>
);
}
Code language: JavaScript (javascript)
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:
npm start
You should be able to select a file with the name displayed on the screen.
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:
import multer from "multer";
const cloudinary = require("cloudinary").v2;
const storage = multer.memoryStorage();
const upload = multer({ storage });
const myUploadMiddleware = upload.single("file");
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET,
secure: true,
});
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
export default async function handler(req, res) {
let response;
if (req.method === "POST") {
await runMiddleware(req, res, myUploadMiddleware);
const file = req.file;
try {
const b64 = Buffer.from(file.buffer).toString("base64");
let dataURI = "data:" + file.mimetype + ";base64," + b64;
response = await cloudinary.uploader.upload(dataURI, {
folder: "file-sharing-app",
resource_type: "auto",
context: `filename=${file.originalname}`,
});
} catch (error) {
res.status(400).json(error);
return;
}
}
return res.status(200).json(response);
}
export const config = {
api: {
bodyParser: false,
},
};
Code language: JavaScript (javascript)
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.
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:
# change the 3000 if you're on a different port
NEXT_PUBLIC_URL=http://localhost:3000
CLOUD_NAME = YOUR CLOUD NAME HERE
API_KEY = YOUR API API KEY
API_SECRET = YOUR API API SECRET
Code language: PHP (php)
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:
import styles from "../styles/Home.module.css";
export default function FileLinkPreview({ file, id, setStatus }) {
const url = `${process.env.NEXT_PUBLIC_URL}/fileDownload?id=${id}`;
const handleCopy = async () => {
await window.navigator.clipboard.writeText(url);
};
return (
<div className={styles.box}>
<h4>{file.name}</h4>
<p>File uploaded to Cloudinary, share this link to others.</p>
<p>{url}</p>
<button className={styles.copy} onClick={handleCopy}>
Copy!
</button>
<button onClick={() => setStatus("")}>Upload new file</button>
</div>
);
}
Code language: JavaScript (javascript)
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:
//...
//import FileLinkPreview
import FileLinkPreview from "../components/FileLinkPreview";
export default function Home() {
const [file, setFile] = useState();
const [status, setStatus] = useState();
const [fileId, setFileId] = useState();
const handleUpload = (e) => {
//...
};
const uploadFile = async () => {
//...
};
return (
<div className={styles.app}>
<h1>Want to share a file?</h1>
{/* Add this */}
{status !== "Upload successful" ? (
<FileUpload
file={file}
setFile={setFile}
handleUpload={handleUpload}
status={status}
/>
) : (
<FileLinkPreview file={file} id={fileId} setStatus={setStatus} />
)}
</div>
);
}
Code language: JavaScript (javascript)
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.
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:
import multer from "multer";
const cloudinary = require("cloudinary").v2;
const storage = multer.memoryStorage();
const upload = multer({ storage });
const myUploadMiddleware = upload.single("file");
cloudinary.config({
//...
});
function runMiddleware(req, res, fn) {
//...
}
export default async function handler(req, res) {
let response;
if (req.method === "POST") {
await runMiddleware(req, res, myUploadMiddleware);
const file = req.file;
try {
//...
} catch (error) {
//...
}
}
// Add this
if (req.method === "GET") {
try {
response = await cloudinary.search
.expression(`public_id=${req.query.id}`)
.with_field("context")
.execute();
} catch (error) {
res.status(400).json(error);
return;
}
}
return res.status(200).json(response);
}
export const config = {
//...
};
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).
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:
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
import styles from "../styles/Home.module.css";
export default function FileDownload() {
const [fileResponse, setFileResponse] = useState();
const [axiosCallStatus, setAxiosCallStatus] = useState();
const { id } = useRouter().query;
useEffect(() => {
if (id === undefined) return;
(async () => {
setAxiosCallStatus("Loading...");
try {
const response = await axios.get(`/api/upload?id=${id}`);
console.log(response.data.resources[0]);
setFileResponse(response.data.resources[0]);
setAxiosCallStatus("");
} catch (error) {
setAxiosCallStatus("File not found, refresh your browser");
}
})();
}, [id]);
return (
<div className={styles.app}>
<div className={styles.box}>
<h2>Your file is Ready</h2>
{fileResponse && fileResponse.length !== 0 ? (
<>
<h4>{fileResponse.context.filename}</h4>
<a
href={fileResponse.secure_url.replace(
"/upload/",
`/upload/fl_attachment:${
fileResponse.context.filename.split(".")[0]
}/`
)}
>
Click to download
</a>
</>
) : (
<p>{axiosCallStatus || "No file found"}</p>
)}
</div>
</div>
);
}
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.