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:
- A form interface for selecting files
- Client-side handling of file data
- Server-side API routes for processing uploads
- 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:
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:
npm install formidable@v2
Then create an API route at pages/api/upload.js
:
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:
- Validate file types
- Restrict file sizes
- Process different file types differently
Here's an example of file validation:
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:
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:
// 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:
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Then create a service for S3 uploads:
// 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:
// 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:
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:
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:
// 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:
- Validate files on both client and server sides for security
- Set appropriate file size limits to prevent server overload
- Use cloud storage solutions like AWS S3, Google Cloud Storage, or Azure Blob Storage for production
- Implement virus scanning for uploaded files when necessary
- Show upload progress for large files to improve user experience
- Generate unique filenames to prevent overwriting
- Use environment variables for storage credentials
- 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
- Next.js API Routes Documentation
- Formidable Documentation
- AWS S3 JavaScript SDK Documentation
- Next.js App Router Documentation
- MDN File API Documentation
Exercise
-
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
-
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! :)