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:

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

  1. 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.

  2. Environment Variables Not Loading: Make sure you’ve created a .env.local file in the root of your project and restarted your development server.

  3. Access Denied Errors: Ensure your Rabata.io credentials are correct and the bucket exists.

Debugging Tips

  1. Check Server Logs: Use console.log statements in your API routes to debug issues.

  2. 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
  });
}