Skip to main content

Next.js File Uploads

File uploads are a common requirement in web applications, whether it's for profile pictures, document sharing, or media uploads. In this guide, we'll explore how to implement file uploads in Next.js applications, covering both client-side and server-side aspects.

Introduction to File Uploads in Next.js

File uploads in Next.js involve several key components:

  1. A form interface for selecting files
  2. Client-side handling of file data
  3. Server-side API routes for processing uploads
  4. Storage solutions for the uploaded files

Next.js provides several approaches to handle file uploads, and the best choice depends on your specific requirements and infrastructure.

Basic File Upload Form

Let's start with creating a simple file upload form in Next.js:

jsx
import { useState } from 'react';

export default function FileUploadForm() {
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [uploadedFileUrl, setUploadedFileUrl] = useState(null);

const handleFileChange = (e) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
}
};

const handleSubmit = async (e) => {
e.preventDefault();
if (!file) return;

setUploading(true);

try {
const formData = new FormData();
formData.append('file', file);

const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error('Upload failed');
}

const data = await response.json();
setUploadedFileUrl(data.fileUrl);

} catch (error) {
console.error('Error uploading file:', error);
} finally {
setUploading(false);
}
};

return (
<div>
<h1>File Upload Example</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="file">Select a file:</label>
<input
type="file"
id="file"
name="file"
onChange={handleFileChange}
/>
</div>
<button
type="submit"
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</form>

{uploadedFileUrl && (
<div>
<h2>Uploaded File:</h2>
<img
src={uploadedFileUrl}
alt="Uploaded file"
style={{ maxWidth: '300px' }}
/>
</div>
)}
</div>
);
}

This form includes:

  • A file input for selection
  • State handling for the selected file
  • Uploading state management
  • Display of the uploaded file (assuming it's an image)

Server-Side API Route for File Uploads

Now, let's create a server-side API route to handle the file uploads. We'll use the formidable library to parse the incoming form data:

First, install the necessary package:

bash
npm install formidable@v2

Then create an API route at pages/api/upload.js:

javascript
import formidable from 'formidable';
import path from 'path';
import fs from 'fs/promises';

// Disable Next.js body parsing for this route
export const config = {
api: {
bodyParser: false,
},
};

export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(process.cwd(), 'public/uploads');
await fs.mkdir(uploadsDir, { recursive: true });

// Configure formidable
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true,
maxFileSize: 10 * 1024 * 1024, // 10MB limit
});

// Parse the form
const [fields, files] = await new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) reject(err);
resolve([fields, files]);
});
});

const file = files.file[0];
const fileName = file.originalFilename;

// Generate relative URL to the file
const fileUrl = `/uploads/${path.basename(file.filepath)}`;

return res.status(200).json({
message: 'File uploaded successfully',
fileName,
fileUrl
});
} catch (error) {
console.error('Upload error:', error);
return res.status(500).json({ error: 'Upload failed' });
}
}

Handling Different File Types

For a more comprehensive file upload system, you may want to:

  1. Validate file types
  2. Restrict file sizes
  3. Process different file types differently

Here's an example of file validation:

javascript
const allowedFileTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
const maxFileSize = 5 * 1024 * 1024; // 5MB

const validateFile = (file) => {
// Check file type
if (!allowedFileTypes.includes(file.type)) {
throw new Error('File type not allowed');
}

// Check file size
if (file.size > maxFileSize) {
throw new Error('File size exceeds limit');
}

return true;
};

You can incorporate this validation into your client-side form handler:

jsx
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) return;

try {
// Validate file before uploading
validateFile(file);

setUploading(true);
// ... upload code as before
} catch (error) {
alert(error.message);
console.error('Validation error:', error);
}
};

Using Next.js App Router

If you're using the newer Next.js App Router, your server-side code will look a bit different. Here's how to implement a file upload route handler:

javascript
// app/api/upload/route.js
import { writeFile } from 'fs/promises';
import { NextResponse } from 'next/server';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';

export async function POST(request) {
try {
const formData = await request.formData();
const file = formData.get('file');

if (!file) {
return NextResponse.json(
{ error: "File is required" },
{ status: 400 }
);
}

const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);

// Generate a unique filename
const fileName = `${uuidv4()}_${file.name}`;
const filePath = path.join(process.cwd(), 'public/uploads', fileName);

// Write the file
await writeFile(filePath, buffer);

const fileUrl = `/uploads/${fileName}`;

return NextResponse.json({
message: "Upload successful",
fileUrl
});
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: "Upload failed" },
{ status: 500 }
);
}
}

Integrating with Cloud Storage

For production applications, storing files locally isn't ideal. Here's an example of integrating with AWS S3:

First, install the AWS SDK:

bash
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Then create a service for S3 uploads:

javascript
// lib/s3Service.js
import {
S3Client,
PutObjectCommand,
GetObjectCommand
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});

export async function uploadToS3(file, fileName, contentType) {
const buffer = Buffer.from(await file.arrayBuffer());

const params = {
Bucket: process.env.S3_BUCKET_NAME,
Key: fileName,
Body: buffer,
ContentType: contentType,
};

try {
await s3Client.send(new PutObjectCommand(params));

// Generate a temporary URL for accessing the file
const getObjectParams = {
Bucket: process.env.S3_BUCKET_NAME,
Key: fileName,
};

const url = await getSignedUrl(
s3Client,
new GetObjectCommand(getObjectParams),
{ expiresIn: 3600 } // URL expires in 1 hour
);

return url;
} catch (error) {
console.error('S3 upload error:', error);
throw error;
}
}

Modify your API route to use this service:

javascript
// app/api/upload/route.js
import { NextResponse } from 'next/server';
import { v4 as uuidv4 } from 'uuid';
import { uploadToS3 } from '@/lib/s3Service';

export async function POST(request) {
try {
const formData = await request.formData();
const file = formData.get('file');

if (!file) {
return NextResponse.json(
{ error: "File is required" },
{ status: 400 }
);
}

// Generate a unique filename
const fileName = `${uuidv4()}_${file.name}`;

// Upload to S3
const fileUrl = await uploadToS3(
file,
fileName,
file.type
);

return NextResponse.json({
message: "Upload successful",
fileUrl
});
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json(
{ error: "Upload failed" },
{ status: 500 }
);
}
}

Implementing Progress Indicators

For a better user experience, especially with large files, you might want to display an upload progress indicator:

jsx
import { useState } from 'react';
import axios from 'axios';

export default function FileUploadWithProgress() {
const [file, setFile] = useState(null);
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const [uploadedFileUrl, setUploadedFileUrl] = useState(null);

const handleFileChange = (e) => {
if (e.target.files?.[0]) {
setFile(e.target.files[0]);
setProgress(0);
}
};

const handleSubmit = async (e) => {
e.preventDefault();
if (!file) return;

setUploading(true);
setProgress(0);

const formData = new FormData();
formData.append('file', file);

try {
const response = await axios.post('/api/upload', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percentCompleted);
},
});

setUploadedFileUrl(response.data.fileUrl);
} catch (error) {
console.error('Upload error:', error);
} finally {
setUploading(false);
}
};

return (
<div>
<h1>File Upload with Progress</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="file">Select a file:</label>
<input
type="file"
id="file"
name="file"
onChange={handleFileChange}
disabled={uploading}
/>
</div>

<button
type="submit"
disabled={!file || uploading}
>
{uploading ? 'Uploading...' : 'Upload'}
</button>

{uploading && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
</form>

{uploadedFileUrl && (
<div>
<h2>Uploaded File:</h2>
<a href={uploadedFileUrl} target="_blank" rel="noopener noreferrer">
View Uploaded File
</a>
</div>
)}
</div>
);
}

Supporting Multiple File Uploads

To handle multiple file uploads, we need to modify both the client and server components:

Client-side form:

jsx
import { useState } from 'react';

export default function MultipleFileUploadForm() {
const [files, setFiles] = useState([]);
const [uploading, setUploading] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState([]);

const handleFileChange = (e) => {
if (e.target.files?.length) {
setFiles(Array.from(e.target.files));
}
};

const handleSubmit = async (e) => {
e.preventDefault();
if (!files.length) return;

setUploading(true);

try {
const formData = new FormData();

// Append all files to FormData
files.forEach((file, index) => {
formData.append(`file-${index}`, file);
});

const response = await fetch('/api/upload-multiple', {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error('Upload failed');
}

const data = await response.json();
setUploadedFiles(data.fileUrls);

} catch (error) {
console.error('Error uploading files:', error);
} finally {
setUploading(false);
}
};

return (
<div>
<h1>Multiple File Upload Example</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="files">Select files:</label>
<input
type="file"
id="files"
name="files"
multiple
onChange={handleFileChange}
/>
</div>

{files.length > 0 && (
<div>
<p>Selected files:</p>
<ul>
{files.map((file, index) => (
<li key={index}>
{file.name} ({Math.round(file.size / 1024)} KB)
</li>
))}
</ul>
</div>
)}

<button
type="submit"
disabled={!files.length || uploading}
>
{uploading ? 'Uploading...' : 'Upload All Files'}
</button>
</form>

{uploadedFiles.length > 0 && (
<div>
<h2>Uploaded Files:</h2>
<ul>
{uploadedFiles.map((fileUrl, index) => (
<li key={index}>
<a href={fileUrl} target="_blank" rel="noopener noreferrer">
File {index + 1}
</a>
</li>
))}
</ul>
</div>
)}
</div>
);
}

Server-side API route:

javascript
// pages/api/upload-multiple.js
import formidable from 'formidable';
import path from 'path';
import fs from 'fs/promises';

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

export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(process.cwd(), 'public/uploads');
await fs.mkdir(uploadsDir, { recursive: true });

// Configure formidable to handle multiple files
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true,
multiples: true,
maxFileSize: 10 * 1024 * 1024, // 10MB limit per file
});

// Parse the form
const [fields, files] = await new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) reject(err);
resolve([fields, files]);
});
});

// Process all uploaded files
const fileUrls = [];

// Get all file entries from the files object
const fileEntries = Object.entries(files);

for (const [fieldName, fileArray] of fileEntries) {
// Each field might contain an array of files
for (const file of fileArray) {
const fileUrl = `/uploads/${path.basename(file.filepath)}`;
fileUrls.push(fileUrl);
}
}

return res.status(200).json({
message: 'Files uploaded successfully',
fileCount: fileUrls.length,
fileUrls
});
} catch (error) {
console.error('Upload error:', error);
return res.status(500).json({ error: 'Upload failed' });
}
}

Best Practices for File Uploads

When implementing file uploads in Next.js, consider these best practices:

  1. Validate files on both client and server sides for security
  2. Set appropriate file size limits to prevent server overload
  3. Use cloud storage solutions like AWS S3, Google Cloud Storage, or Azure Blob Storage for production
  4. Implement virus scanning for uploaded files when necessary
  5. Show upload progress for large files to improve user experience
  6. Generate unique filenames to prevent overwriting
  7. Use environment variables for storage credentials
  8. Implement proper error handling to provide clear feedback

Summary

In this guide, we've covered:

  • Creating basic file upload forms in Next.js
  • Setting up server-side API routes to handle uploads
  • Validating files for security
  • Working with cloud storage solutions
  • Implementing progress indicators
  • Supporting multiple file uploads
  • Best practices for file handling

File uploads are an essential part of many web applications, and Next.js provides flexible options for implementing them. Whether you're building a simple image upload feature or a complex document management system, the techniques covered in this guide will help you create robust file upload functionality.

Additional Resources

Exercise

  1. Build a profile picture upload feature that:

    • Accepts only image files
    • Shows a preview before uploading
    • Resizes the image on the server
    • Stores the image URL in a database
  2. Create a file sharing application that:

    • Allows uploading multiple files of various types
    • Generates shareable links
    • Sets expiration dates for uploaded files
    • Shows upload history


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)