Next.js Quickstart
Learn how to integrate Rabata.io object storage with your Next.js applications for file uploads and storage.
Introduction
This guide will show you how to use Rabata.io as a storage backend for your Next.js application. You’ll learn how to upload files directly from the browser, handle file uploads through API routes, and display stored files in your application.
Prerequisites
Before you begin, make sure you have:
- Node.js 18.17 or later installed
- A Rabata.io account with access credentials
- A bucket created in your Rabata.io account
- Basic knowledge of Next.js and React
Installation
Create a New Next.js Application
If you’re starting from scratch, create a new Next.js application:
$ npx create-next-app@latest my-nextjs-app
$ cd my-nextjs-app
Install Required Dependencies
Install the AWS SDK for JavaScript and other necessary packages:
$ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner uuid
Configuration
Set Up Environment Variables
Create a .env.local file in the root of your project to store your Rabata.io credentials:
RABATA_ACCESS_KEY=YOUR_ACCESS_KEY
RABATA_SECRET_KEY=YOUR_SECRET_KEY
RABATA_BUCKET_NAME=your-bucket-name
RABATA_REGION=eu-west-1
RABATA_ENDPOINT=https://s3.eu-west-1.rabata.io
Create a Configuration File
Create a utility file to configure the S3 client:
// lib/s3.js
import { S3Client } from '@aws-sdk/client-s3';
export const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true // Required for Rabata.io
});
Basic Usage
Server-Side File Upload with API Routes
Create an API route to handle file uploads:
// app/api/upload/route.js (App Router)
import { NextResponse } from 'next/server';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
// Initialize the S3 client
const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true
});
export async function POST(request) {
try {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
// Convert file to buffer
const buffer = Buffer.from(await file.arrayBuffer());
const fileName = `${uuidv4()}-${file.name}`;
// Upload to Rabata.io
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.RABATA_BUCKET_NAME,
Key: fileName,
Body: buffer,
ContentType: file.type
})
);
return NextResponse.json({
message: 'File uploaded successfully',
fileName: fileName,
fileUrl: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${fileName}`
});
} catch (error) {
console.error('Error uploading file:', error);
return NextResponse.json({ error: 'Error uploading file' }, { status: 500 });
}
}
For Pages Router:
// pages/api/upload.js (Pages Router)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
import formidable from 'formidable';
import fs from 'fs';
// Disable the default body parser
export const config = {
api: {
bodyParser: false,
},
};
// Initialize the S3 client
const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true
});
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const form = formidable({});
const [fields, files] = await form.parse(req);
const file = files.file?.[0];
if (!file) {
return res.status(400).json({ error: 'No file provided' });
}
// Read file from disk
const fileContent = fs.readFileSync(file.filepath);
const fileName = `${uuidv4()}-${file.originalFilename}`;
// Upload to Rabata.io
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.RABATA_BUCKET_NAME,
Key: fileName,
Body: fileContent,
ContentType: file.mimetype
})
);
return res.status(200).json({
message: 'File uploaded successfully',
fileName: fileName,
fileUrl: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${fileName}`
});
} catch (error) {
console.error('Error uploading file:', error);
return res.status(500).json({ error: 'Error uploading file' });
}
}
Client-Side File Upload Component
Create a React component for file uploads:
// components/FileUpload.jsx
'use client'; // For App Router
import { useState } from 'react';
export default function FileUpload() {
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [uploadedFileUrl, setUploadedFileUrl] = useState('');
const [error, setError] = useState('');
const handleFileChange = (e) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
setError('');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) {
setError('Please select a file');
return;
}
setUploading(true);
setError('');
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error uploading file');
}
setUploadedFileUrl(data.fileUrl);
setFile(null);
} catch (error) {
console.error('Error:', error);
setError(error.message);
} finally {
setUploading(false);
}
};
return (
<div className="upload-container">
<h2>Upload File to Rabata.io</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="file">Select File</label>
<input
type="file"
id="file"
onChange={handleFileChange}
disabled={uploading}
/>
</div>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={!file || uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</form>
{uploadedFileUrl && (
<div className="success">
<p>File uploaded successfully!</p>
<a href={uploadedFileUrl} target="_blank" rel="noopener noreferrer">
View Uploaded File
</a>
</div>
)}
</div>
);
}
Generating Presigned URLs for Direct Uploads
For larger files, you might want to generate presigned URLs for direct uploads to Rabata.io:
// app/api/presigned-upload/route.js (App Router)
import { NextResponse } from 'next/server';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuidv4 } from 'uuid';
// Initialize the S3 client
const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true
});
export async function POST(request) {
try {
const { fileName, fileType } = await request.json();
if (!fileName || !fileType) {
return NextResponse.json({ error: 'File name and type are required' }, { status: 400 });
}
const key = `${uuidv4()}-${fileName}`;
const command = new PutObjectCommand({
Bucket: process.env.RABATA_BUCKET_NAME,
Key: key,
ContentType: fileType
});
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
return NextResponse.json({
presignedUrl,
key,
fileUrl: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${key}`
});
} catch (error) {
console.error('Error generating presigned URL:', error);
return NextResponse.json({ error: 'Error generating presigned URL' }, { status: 500 });
}
}
Listing Files from Rabata.io
Create an API route to list files from your bucket:
// app/api/files/route.js (App Router)
import { NextResponse } from 'next/server';
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
// Initialize the S3 client
const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true
});
export async function GET() {
try {
const command = new ListObjectsV2Command({
Bucket: process.env.RABATA_BUCKET_NAME
});
const response = await s3Client.send(command);
const files = response.Contents?.map(item => ({
key: item.Key,
size: item.Size,
lastModified: item.LastModified,
url: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${item.Key}`
})) || [];
return NextResponse.json({ files });
} catch (error) {
console.error('Error listing files:', error);
return NextResponse.json({ error: 'Error listing files' }, { status: 500 });
}
}
Advanced Usage
CORS Configuration for Direct Uploads
If you’re using direct uploads with presigned URLs, you need to configure CORS for your Rabata.io bucket:
[
{
"AllowedHeaders": [
"Authorization",
"Content-Type",
"Content-Length",
"Content-MD5",
"x-amz-content-sha256",
"x-amz-date",
"x-amz-security-token",
"x-amz-user-agent"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"http://localhost:3000",
"https://your-production-domain.com"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3600
}
]
Image Optimization with Next.js
Next.js provides built-in image optimization through the next/image component. You can use it with Rabata.io by configuring a custom loader:
// components/OptimizedImage.jsx
import Image from 'next/image';
const rabataLoader = ({ src, width, quality }) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
export default function OptimizedImage({ src, alt, width, height, ...props }) {
return (
<Image
loader={rabataLoader}
src={src}
alt={alt}
width={width}
height={height}
{...props}
/>
);
}
Handling File Deletion
Create an API route to delete files from your bucket:
// app/api/delete-file/route.js (App Router)
import { NextResponse } from 'next/server';
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
// Initialize the S3 client
const s3Client = new S3Client({
region: process.env.RABATA_REGION || 'eu-west-1',
endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
credentials: {
accessKeyId: process.env.RABATA_ACCESS_KEY || '',
secretAccessKey: process.env.RABATA_SECRET_KEY || ''
},
forcePathStyle: true
});
export async function DELETE(request) {
try {
const { key } = await request.json();
if (!key) {
return NextResponse.json({ error: 'File key is required' }, { status: 400 });
}
await s3Client.send(
new DeleteObjectCommand({
Bucket: process.env.RABATA_BUCKET_NAME,
Key: key
})
);
return NextResponse.json({ message: 'File deleted successfully' });
} catch (error) {
console.error('Error deleting file:', error);
return NextResponse.json({ error: 'Error deleting file' }, { status: 500 });
}
}
Complete Example
Here’s a complete example of a Next.js application that uses Rabata.io for file storage:
// app/page.js (App Router)
import FileUpload from '../components/FileUpload';
import FileList from '../components/FileList';
export default function Home() {
return (
<main className="container">
<h1>Next.js with Rabata.io</h1>
<FileUpload />
<FileList />
</main>
);
}
// components/FileList.jsx
'use client'; // For App Router
import { useState, useEffect } from 'react';
export default function FileList() {
const [files, setFiles] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchFiles = async () => {
try {
const response = await fetch('/api/files');
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error fetching files');
}
setFiles(data.files);
} catch (error) {
console.error('Error:', error);
setError(error.message);
} finally {
setLoading(false);
}
};
fetchFiles();
}, []);
const handleDelete = async (key) => {
try {
const response = await fetch('/api/delete-file', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Error deleting file');
}
// Remove the deleted file from the list
setFiles(files.filter(file => file.key !== key));
} catch (error) {
console.error('Error:', error);
alert(error.message);
}
};
if (loading) return <p>Loading files...</p>;
if (error) return <p className="error">Error: {error}</p>;
return (
<div className="file-list">
<h2>Files in Rabata.io Bucket</h2>
{files.length === 0 ? (
<p>No files found in the bucket.</p>
) : (
<ul>
{files.map((file) => (
<li key={file.key}>
<a href={file.url} target="_blank" rel="noopener noreferrer">
{file.key}
</a>
<span className="file-size">
{(file.size / 1024).toFixed(2)} KB
</span>
<button
className="delete-button"
onClick={() => handleDelete(file.key)}
>
Delete
</button>
</li>
))}
</ul>
)}
</div>
);
}
Troubleshooting
Common Issues
-
CORS Errors: If you’re getting CORS errors when uploading directly to Rabata.io, make sure you’ve configured CORS for your bucket as shown in the Advanced Usage section.
-
Environment Variables Not Loading: Make sure you’ve created a
.env.localfile in the root of your project and restarted your development server. -
Access Denied Errors: Ensure your Rabata.io credentials are correct and the bucket exists.
Debugging Tips
-
Check Server Logs: Use
console.logstatements in your API routes to debug issues. -
Verify Environment Variables: Add a debug endpoint to verify your environment variables are loaded correctly:
// app/api/debug/route.js (App Router)
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
region: process.env.RABATA_REGION,
endpoint: process.env.RABATA_ENDPOINT,
bucketName: process.env.RABATA_BUCKET_NAME,
// Don't include sensitive credentials in production!
hasAccessKey: !!process.env.RABATA_ACCESS_KEY,
hasSecretKey: !!process.env.RABATA_SECRET_KEY
});
}