Monday, 12 August, 2024

Implementing Multiple File Uploads in Next.js – A Step-by-Step Guide

Implementing Multiple File Uploads in Next.js - A Step-by-Step Guide

When building modern web applications, the ability to upload multiple files simultaneously is a common requirement. Next.js, a popular React framework, offers a robust platform to implement this feature seamlessly. In this guide, we’ll walk through the process of setting up multiple file uploads in a Next.js application.

In this article, we won’t be diving into styling details like progress bars and other enhancements for file uploads. However, we’ll cover these aspects in a future article, where we’ll implement them smoothly and effectively.

Prerequisites

Before diving into the code, ensure you have the following installed:

  • Node.js (v12.x or higher)
  • Next.js (v10.x or higher)
  • Basic knowledge of React and Next.js

1. Setting Up the Project

If you haven’t already set up a Next.js project, you can create a new one by running:

npx create-next-app my-multi-upload-app
cd my-multi-upload-app

Install any necessary dependencies:

npm install

2. Creating the Upload Component

Next, we’ll create a React component to handle file uploads. Inside the src directory, create a new file called index.tsx. This file will contain the HTML and logic for the file upload component:

export default function Home() {
return (
    <div className="max-w-screen-xl mx-auto my-5">
      <div className="w-full text-3xl">
        <h1>Simple File Upload In Next.JS</h1>
      </div>
      <div className="w-full bg-gray-100 rounded-xl my-7 p-10">
        <h4>File Upload</h4>
        <div className="mt-7">
          <input type="file" multiple className="bg-blue-800 text-white mr-5" />
          <button className="bg-blue-800 text-white rounded-md py-1 px-4 hover:bg-green-700">Upload</button>
        </div>
      </div>
    </div>
  );
}

3. Handling File Uploads

Now, let’s extend this component to handle file selection and uploading.

We’ll modify the index.tsx file to manage file data and send it to the Next.js API to save the files in a specified folder (in this case, a folder named upload inside public).

Here’s the updated code:


export default function Home() {
  const [fileData, setFileData] = useState<[]>([]);
  const fileUpload:any = useRef<HTMLFormElement>(null);
  const [fileUploadStatus, setFileUploadStatus] = useState<any | null>(null);

  const handleFileUploadChange = (data:any) =>{
    if(data.target.files){
      if(data.target.files.length > 0){
        let fileData:any = [];
        for (let index = 0; index < data.target.files.length; index++) {
          fileData.push({name: data.target.files[index].name, size: data.target.files[index].size})
        }
        setFileData(fileData)
      }
    }
  }

  const handleFileUpload = async ()=>{
    const formData = new FormData();
      fileData.map((item:any, index:any)=>{
        formData.append('file', fileUpload.current.files[index], fileUpload.current.files[index].name);
      })
      const resultData = await axios.post('/api/fuleUploadAPI', formData,{
        headers: {
            "Content-Type": "multipart/form-data"
        }
      });
      console.log("resultData", resultData);
      if(resultData.data.data){
        setFileUploadStatus(resultData.data.data);
      }
  }

  return (
    <div className="max-w-screen-xl mx-auto my-5">
      <div className="w-full text-3xl">
        <h1>Simple File Upload In Next.JS</h1>
      </div>
      <div className="w-full bg-gray-100 rounded-xl my-7 p-10">
        <h4>File Upload</h4>
        <div className="mt-7">
          <input type="file" multiple className="bg-blue-800 text-white mr-5" onChange={handleFileUploadChange} ref={fileUpload} />
          <button className="bg-blue-800 text-white rounded-md py-1 px-4 hover:bg-green-700" onClick={handleFileUpload}>Upload</button>
        </div>
        <div className="mt-5">
          {fileData && fileData.length > 0 && (
            fileData.map((item:any, index:any)=>{
              return(
                <div key={index}>{index+1} - {item.name}</div>
              )
            })
          )}
          
        </div>
        <div>
          {fileUploadStatus && fileUploadStatus.file.length && (
            <div className="bg-green-800 text-white p-5 rounded-lg mt-5">File Uploaded</div>
          )}
        </div>
      </div>
    </div>
  );
}

What’s Happening in the Code?

  1. State Variables:
    • fileData: Stores information about selected files (name and size).
    • fileUploadStatus: Tracks the status of the file upload.
  2. Ref Variable:
    • fileUpload: References the file input element, enabling direct access to the selected files.
  3. handleFileUploadChange Function:
    • Triggered when the user selects files.
    • Updates fileData with the name and size of each selected file.
  4. handleFileUpload Function:
    • Creates a FormData object to prepare the files for upload.
    • Sends a POST request to the /api/fileUploadAPI endpoint with the selected files.
    • Updates fileUploadStatus upon successful upload.

4. Creating the API Endpoint

To handle the file uploads on the server, we need to create an API endpoint.

We’ll use the formidable library to manage multipart form data. Install formidable by running:

npm i formidable

Now, create a new API route in pages/api/fileUploadAPI.tsx and import the necessary modules:

Imports

  • fs (File System): Used for file system operations such as creating directories and moving files.
  • path: Provides utilities for working with file and directory paths.
  • formidable: A library for handling file uploads in Node.js.
// api/fuleUploadAPI.tsx
import fs from 'fs';
import path from 'path';
import formidable, { Fields, Files } from 'formidable';

export const config

  • api: { bodyParser: false }: This tells Next.js not to use its default body parser, because formidable will handle parsing the incoming request data. This is crucial when dealing with file uploads, which are typically sent as multipart/form-data.
// api/fuleUploadAPI.tsx
export const config = {
  api: {
    bodyParser: false,
  },
};

Formidable Setup:

  • form = formidable({ ... }): This initializes formidable with specific options:
    • uploadDir: The directory where uploaded files will be saved. It’s set to '/public/uploads' in the root directory of the project.
    • keepExtensions: If set to true, this keeps the original file extensions when saving the files.
    • filename: A function that customizes the filename for each uploaded file. It uses the fileNameChange function.
  • fileNameChange: This function generates a unique filename using the current timestamp and the original filename.
// api/fuleUploadAPI.tsx
const form:any = formidable({
    uploadDir: path.join(process.cwd(), '/public/uploads'),
    keepExtensions: true,
    filename: (name:any, ext:any, part:any) => fileNameChange(name, ext, part),
});
const fileNameChange = (name:any, ext:any, part:any) =>{
    return `${Date.now()}-${part.originalFilename}`
}

Directory Creation:

  • fs.promises.mkdir(form.uploadDir, { recursive: true }): This ensures that the upload directory exists. If it doesn’t exist, it will be created, including any necessary parent directories (recursive: true).
// api/fuleUploadAPI.tsx
await fs.promises.mkdir(form.uploadDir, { recursive: true });

Parsing the Request:

  • const { fields, files } = await new Promise(...): The form data is parsed using form.parse(req, ...), which extracts the fields and files from the incoming request. If there’s an error during parsing, it’s caught and handled by the reject function.
// api/fuleUploadAPI.tsx
const { fields, files } : {fields: Fields, files: Files} = await new Promise((resolve, reject) => {
  form.parse(req, (err: any, fields: Fields, files: Files) => {
    if (err) reject(err);
    else resolve({ fields, files });
  });
});

File Processing:

  • file = files.file: Extracts the uploaded files from the files object. It’s assumed that the input field name in the form is file.
  • uploadDIR: Determines the upload directory, defaulting to /public/uploads if form.uploadDir is not defined.
  • for loop: Iterates over each uploaded file:
    • extention: Extracts the file extension from the new filename.
    • newFilename: Generates a new filename if needed.
    • newFilePath: Constructs the full path where the file will be stored.
    • fs.promises.rename(...): Moves the uploaded file from its temporary location to the final directory.
// api/fuleUploadAPI.tsx
// File processing after parsing
const file:any = files.file;
let uploadDIR:string = form.uploadDir != undefined ? form.uploadDir : '/public/uploads';
for (let index = 0; index < file.length; index++) {
    let extention = file[index].newFilename.split('.').pop();    
    let newFilename:string = file[index].newFilename != undefined ? file[index].newFilename : `${Date.now()}-tempfile-1.jpg`;
    let newFilePath = path.join(uploadDIR, newFilename);
    await fs.promises.rename(file[index].filepath, newFilePath);
}

Response:

  • If the upload and file processing are successful, it returns a 200 status with a success message and the uploaded file information.
// api/fuleUploadAPI.tsx
return res.status(200).json({ message: 'File uploaded successfully', data: {file: file, form: form} });
Here is the final api file code:
// api/fuleUploadAPI.tsx
import fs from 'fs';
import path from 'path';
import formidable, { Fields, Files } from 'formidable';

export const config = {
  api: {
    bodyParser: false,
  },
};

const handler = async (req: any, res: any) => {

  const form:any = formidable({
    uploadDir: path.join(process.cwd(), '/public/uploads'),
    keepExtensions: true,
    filename: (name:any, ext:any, part:any) => fileNameChange(name, ext, part),
  });

  const fileNameChange = (name:any, ext:any, part:any) =>{
    return `${Date.now()}-${part.originalFilename}`
  }

  // Ensure the upload directory exists
  await fs.promises.mkdir(form.uploadDir, { recursive: true });

  try {
    const { fields, files } : {fields: Fields, files: Files} = await new Promise((resolve, reject) => {
      form.parse(req, (err: any, fields: Fields, files: Files) => {
        if (err) reject(err);
        else resolve({ fields, files });
      });
    });

    // File processing after parsing
    const file:any = files.file;
    let uploadDIR:string = form.uploadDir != undefined ? form.uploadDir : '/public/uploads';
    for (let index = 0; index < file.length; index++) {
        let extention = file[index].newFilename.split('.').pop();    
        let newFilename:string = file[index].newFilename != undefined ? file[index].newFilename : `${Date.now()}-tempfile-1.jpg`;
        let newFilePath = path.join(uploadDIR, newFilename);
        await fs.promises.rename(file[index].filepath, newFilePath);
    }

    return res.status(200).json({ message: 'File uploaded successfully', data: {file: file, form: form} });
  } catch (error: any) {
    console.error('Error:', error);
    return res.status(500).json({ error: 'An error occurred during file upload' });
  }
};

export default handler;

4. Testing the Implementation

To test your implementation:

Start the development server:

npm run dev
  1. Open your browser and navigate to http://localhost:3000.
  2. Use the file input to select multiple files and click the “Upload” button.

If everything is set up correctly, you should see a success message, and the files should be saved in the public/uploads directory.

For the complete code, you can check out my GitHub repository.

To enhance the performance of your Next.js application, please refer to our other post.

Conclusion

Implementing multiple file uploads in a Next.js application is a straightforward process when you break it down into manageable steps. By setting up a custom upload component, handling file data on the client side, and creating a server-side API to process and store the files, you can add this essential feature to your web application. With the flexibility of Next.js and the power of tools like formidable, you have everything you need to handle file uploads efficiently.

Whether you’re building a complex application or a simple project, understanding how to manage file uploads will expand your ability to create more dynamic and user-friendly web experiences. As you continue to develop your Next.js skills, you can further refine this functionality, such as adding file type validation, progress bars, or integrating cloud storage solutions.

For a deeper dive, feel free to explore the complete code on GitHub and stay tuned for future articles where we’ll cover additional enhancements and best practices.

Happy coding! 🙂


Write a Comment

Your email address will not be published.