Nuxt.js Quickstart

Learn how to integrate Rabata.io object storage with your Nuxt.js applications for file uploads and storage.

Introduction

This guide will show you how to use Rabata.io as a storage backend for your Nuxt.js application. You’ll learn how to upload files, manage storage, and serve files from Rabata.io in your Nuxt.js projects.

Prerequisites

Before you begin, make sure you have:

Installation

Create a New Nuxt.js Application

If you’re starting from scratch, create a new Nuxt.js application:

$ npx nuxi init my-nuxt-app
$ cd my-nuxt-app
$ npm install

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

Configure Nuxt Runtime Config

Update your nuxt.config.ts file to include the environment variables:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-side environment variables
    rabata: {
      accessKey: process.env.RABATA_ACCESS_KEY,
      secretKey: process.env.RABATA_SECRET_KEY,
      bucketName: process.env.RABATA_BUCKET_NAME,
      region: process.env.RABATA_REGION || 'eu-west-1',
      endpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io'
    },
    // Public variables that are exposed to the client
    public: {
      rabataEndpoint: process.env.RABATA_ENDPOINT || 'https://s3.eu-west-1.rabata.io',
      rabataBucketName: process.env.RABATA_BUCKET_NAME
    }
  }
})

Create a Server Utility for S3

Create a server utility file to configure the S3 client:

// server/utils/s3.ts
import { S3Client } from '@aws-sdk/client-s3';
import { useRuntimeConfig } from '#imports';

let s3Client: S3Client | null = null;

export function getS3Client() {
  if (s3Client) return s3Client;
  
  const config = useRuntimeConfig();
  
  s3Client = new S3Client({
    region: config.rabata.region,
    endpoint: config.rabata.endpoint,
    credentials: {
      accessKeyId: config.rabata.accessKey,
      secretAccessKey: config.rabata.secretKey
    },
    forcePathStyle: true // Required for Rabata.io
  });
  
  return s3Client;
}

Basic Usage

Create Server API Routes

Create a server API route to handle file uploads:

// server/api/upload.post.ts
import { readBody } from 'h3';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
import { getS3Client } from '../utils/s3';
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  try {
    const config = useRuntimeConfig();
    const s3Client = getS3Client();
    
    // Get the multipart form data
    const formData = await readMultipartFormData(event);
    
    if (!formData || formData.length === 0) {
      return createError({
        statusCode: 400,
        message: 'No file provided'
      });
    }
    
    // Find the file part
    const filePart = formData.find(part => part.name === 'file');
    
    if (!filePart || !filePart.filename) {
      return createError({
        statusCode: 400,
        message: 'No file provided'
      });
    }
    
    // Generate a unique filename
    const fileName = `${uuidv4()}-${filePart.filename}`;
    
    // Upload to Rabata.io
    await s3Client.send(
      new PutObjectCommand({
        Bucket: config.rabata.bucketName,
        Key: fileName,
        Body: filePart.data,
        ContentType: filePart.type || 'application/octet-stream'
      })
    );
    
    return {
      message: 'File uploaded successfully',
      fileName: fileName,
      fileUrl: `${config.rabata.endpoint}/${config.rabata.bucketName}/${fileName}`
    };
  } catch (error) {
    console.error('Error uploading file:', error);
    return createError({
      statusCode: 500,
      message: 'Error uploading file'
    });
  }
});

Create an API route to list files from your bucket:

// server/api/files.get.ts
import { ListObjectsV2Command } from '@aws-sdk/client-s3';
import { getS3Client } from '../utils/s3';
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  try {
    const config = useRuntimeConfig();
    const s3Client = getS3Client();
    
    const command = new ListObjectsV2Command({
      Bucket: config.rabata.bucketName
    });
    
    const response = await s3Client.send(command);
    
    const files = response.Contents?.map(item => ({
      key: item.Key,
      size: item.Size,
      lastModified: item.LastModified,
      url: `${config.rabata.endpoint}/${config.rabata.bucketName}/${item.Key}`
    })) || [];
    
    return { files };
  } catch (error) {
    console.error('Error listing files:', error);
    return createError({
      statusCode: 500,
      message: 'Error listing files'
    });
  }
});

Create a File Upload Component

Create a Vue component for file uploads:

<!-- components/FileUpload.vue -->
<template>
  <div class="upload-container">
    <h2>Upload File to Rabata.io</h2>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label for="file">Select File</label>
        <input
          type="file"
          id="file"
          @change="handleFileChange"
          :disabled="uploading"
        />
      </div>
      
      <p v-if="error" class="error">{{ error }}</p>
      
      <button type="submit" :disabled="!file || uploading">
        {{ uploading ? 'Uploading...' : 'Upload' }}
      </button>
    </form>
    
    <div v-if="uploadedFileUrl" class="success">
      <p>File uploaded successfully!</p>
      <a :href="uploadedFileUrl" target="_blank" rel="noopener noreferrer">
        View Uploaded File
      </a>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const file = ref(null);
const uploading = ref(false);
const uploadedFileUrl = ref('');
const error = ref('');

const handleFileChange = (e) => {
  if (e.target.files && e.target.files[0]) {
    file.value = e.target.files[0];
    error.value = '';
  }
};

const handleSubmit = async () => {
  if (!file.value) {
    error.value = 'Please select a file';
    return;
  }
  
  uploading.value = true;
  error.value = '';
  
  try {
    const formData = new FormData();
    formData.append('file', file.value);
    
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message || 'Error uploading file');
    }
    
    uploadedFileUrl.value = data.fileUrl;
    file.value = null;
  } catch (err) {
    console.error('Error:', err);
    error.value = err.message;
  } finally {
    uploading.value = false;
  }
};
</script>

<style scoped>
.upload-container {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
}

.error {
  color: red;
  margin: 10px 0;
}

.success {
  margin-top: 20px;
  padding: 10px;
  background-color: #e6f7e6;
  border-radius: 5px;
}

button {
  padding: 8px 16px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}
</style>

Create a File List Component

Create a Vue component to list files:

<!-- components/FileList.vue -->
<template>
  <div class="file-list">
    <h2>Files in Rabata.io Bucket</h2>
    
    <p v-if="loading">Loading files...</p>
    <p v-else-if="error" class="error">Error: {{ error }}</p>
    <p v-else-if="files.length === 0">No files found in the bucket.</p>
    
    <ul v-else>
      <li v-for="file in files" :key="file.key">
        <a :href="file.url" target="_blank" rel="noopener noreferrer">
          {{ file.key }}
        </a>
        <span class="file-size">
          {{ (file.size / 1024).toFixed(2) }} KB
        </span>
        <button 
          class="delete-button"
          @click="handleDelete(file.key)"
          :disabled="deleting === file.key"
        >
          {{ deleting === file.key ? 'Deleting...' : 'Delete' }}
        </button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const files = ref([]);
const loading = ref(true);
const error = ref('');
const deleting = ref('');

const fetchFiles = async () => {
  try {
    loading.value = true;
    const response = await fetch('/api/files');
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message || 'Error fetching files');
    }
    
    files.value = data.files;
  } catch (err) {
    console.error('Error:', err);
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};

const handleDelete = async (key) => {
  try {
    deleting.value = key;
    const response = await fetch('/api/delete', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ key }),
    });
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message || 'Error deleting file');
    }
    
    // Remove the deleted file from the list
    files.value = files.value.filter(file => file.key !== key);
  } catch (err) {
    console.error('Error:', err);
    alert(err.message);
  } finally {
    deleting.value = '';
  }
};

onMounted(fetchFiles);
</script>

<style scoped>
.file-list {
  max-width: 800px;
  margin: 20px auto;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.file-size {
  margin: 0 10px;
  color: #666;
  font-size: 0.9em;
}

.delete-button {
  margin-left: auto;
  padding: 5px 10px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.delete-button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.error {
  color: red;
}
</style>

Create a Delete File API Route

Create an API route to delete files:

// server/api/delete.post.ts
import { readBody } from 'h3';
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getS3Client } from '../utils/s3';
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  try {
    const config = useRuntimeConfig();
    const s3Client = getS3Client();
    const body = await readBody(event);
    
    if (!body.key) {
      return createError({
        statusCode: 400,
        message: 'File key is required'
      });
    }
    
    await s3Client.send(
      new DeleteObjectCommand({
        Bucket: config.rabata.bucketName,
        Key: body.key
      })
    );
    
    return { message: 'File deleted successfully' };
  } catch (error) {
    console.error('Error deleting file:', error);
    return createError({
      statusCode: 500,
      message: 'Error deleting file'
    });
  }
});

Create a Page to Use the Components

Create a page to use the file upload and list components:

<!-- pages/index.vue -->
<template>
  <div class="container">
    <h1>Nuxt.js with Rabata.io</h1>
    <FileUpload />
    <FileList />
  </div>
</template>

<script setup>
import FileUpload from '~/components/FileUpload.vue';
import FileList from '~/components/FileList.vue';
</script>

<style scoped>
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
}
</style>

Advanced Usage

Generating Presigned URLs for Direct Uploads

For larger files, you might want to generate presigned URLs for direct uploads to Rabata.io:

// server/api/presigned-upload.post.ts
import { readBody } from 'h3';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuidv4 } from 'uuid';
import { getS3Client } from '../utils/s3';
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  try {
    const config = useRuntimeConfig();
    const s3Client = getS3Client();
    const body = await readBody(event);
    
    if (!body.fileName || !body.fileType) {
      return createError({
        statusCode: 400,
        message: 'File name and type are required'
      });
    }
    
    const key = `${uuidv4()}-${body.fileName}`;
    
    const command = new PutObjectCommand({
      Bucket: config.rabata.bucketName,
      Key: key,
      ContentType: body.fileType
    });
    
    const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
    
    return { 
      presignedUrl,
      key,
      fileUrl: `${config.rabata.endpoint}/${config.rabata.bucketName}/${key}`
    };
  } catch (error) {
    console.error('Error generating presigned URL:', error);
    return createError({
      statusCode: 500,
      message: 'Error generating presigned URL'
    });
  }
});

Direct Upload Component with Presigned URLs

Create a component for direct uploads using presigned URLs:

<!-- components/DirectUpload.vue -->
<template>
  <div class="upload-container">
    <h2>Direct Upload to Rabata.io</h2>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label for="direct-file">Select File</label>
        <input
          type="file"
          id="direct-file"
          @change="handleFileChange"
          :disabled="uploading"
        />
      </div>
      
      <p v-if="error" class="error">{{ error }}</p>
      
      <div v-if="progress > 0" class="progress-container">
        <div class="progress-bar" :style="{ width: `${progress}%` }"></div>
        <span>{{ progress }}%</span>
      </div>
      
      <button 
        @click="uploadDirectly" 
        :disabled="!selectedFile || uploading"
        class="upload-button"
      >
        {{ uploading ? 'Uploading...' : 'Upload Directly' }}
      </button>
    </form>
    
    <div v-if="uploadedFileUrl" class="success">
      <p>File uploaded successfully!</p>
      <a :href="uploadedFileUrl" target="_blank" rel="noopener noreferrer">
        View Uploaded File
      </a>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const file = ref(null);
const uploading = ref(false);
const uploadedFileUrl = ref('');
const error = ref('');
const progress = ref(0);

const handleFileChange = (e) => {
  if (e.target.files && e.target.files[0]) {
    file.value = e.target.files[0];
    error.value = '';
    progress.value = 0;
  }
};

const handleSubmit = async () => {
  if (!file.value) {
    error.value = 'Please select a file';
    return;
  }
  
  uploading.value = true;
  error.value = '';
  progress.value = 0;
  
  try {
    // Step 1: Get a presigned URL
    const presignedResponse = await fetch('/api/presigned-upload', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        fileName: file.value.name,
        fileType: file.value.type
      }),
    });
    
    const presignedData = await presignedResponse.json();
    
    if (!presignedResponse.ok) {
      throw new Error(presignedData.message || 'Error getting presigned URL');
    }
    
    // Step 2: Upload directly to Rabata.io using the presigned URL
    const xhr = new XMLHttpRequest();
    
    // Track upload progress
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        progress.value = Math.round((event.loaded / event.total) * 100);
      }
    };
    
    // Create a Promise to handle the XHR request
    await new Promise((resolve, reject) => {
      xhr.open('PUT', presignedData.presignedUrl, true);
      xhr.setRequestHeader('Content-Type', file.value.type);
      
      xhr.onload = () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve();
        } else {
          reject(new Error(`HTTP Error: ${xhr.status}`));
        }
      };
      
      xhr.onerror = () => reject(new Error('Network Error'));
      xhr.send(file.value);
    });
    
    uploadedFileUrl.value = presignedData.fileUrl;
    file.value = null;
  } catch (err) {
    console.error('Error:', err);
    error.value = err.message;
  } finally {
    uploading.value = false;
  }
};
</script>

<style scoped>
.upload-container {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
}

.error {
  color: red;
  margin: 10px 0;
}

.success {
  margin-top: 20px;
  padding: 10px;
  background-color: #e6f7e6;
  border-radius: 5px;
}

.progress-container {
  height: 20px;
  background-color: #f0f0f0;
  border-radius: 4px;
  margin: 10px 0;
  position: relative;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background-color: #4CAF50;
  transition: width 0.3s ease;
}

.progress-container span {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  text-align: center;
  line-height: 20px;
  font-size: 12px;
  color: #333;
}

button {
  padding: 8px 16px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}
</style>

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
  }
]

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 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 server API routes to debug issues.

  2. Verify Environment Variables: Create a debug API route to verify your environment variables are loaded correctly:

// server/api/debug.get.ts
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  
  return {
    region: config.rabata.region,
    endpoint: config.rabata.endpoint,
    bucketName: config.rabata.bucketName,
    // Don't include sensitive credentials in production!
    hasAccessKey: !!config.rabata.accessKey,
    hasSecretKey: !!config.rabata.secretKey
  };
});

Production Considerations

Security Best Practices

  1. Validate File Types: Always validate uploaded file types to prevent security issues:
// server/api/upload.post.ts
// Add this validation before uploading
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(filePart.type)) {
  return createError({
    statusCode: 400,
    message: 'File type not allowed'
  });
}
  1. Set File Size Limits: Limit the size of uploaded files:
// server/api/upload.post.ts
// Add this validation before uploading
const maxSize = 5 * 1024 * 1024; // 5MB
if (filePart.data.length > maxSize) {
  return createError({
    statusCode: 400,
    message: 'File size exceeds limit (5MB)'
  });
}
  1. Use Signed URLs with Expiration: For private files, use signed URLs with expiration:
// server/api/signed-url.post.ts
import { readBody } from 'h3';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { getS3Client } from '../utils/s3';
import { useRuntimeConfig } from '#imports';

export default defineEventHandler(async (event) => {
  try {
    const config = useRuntimeConfig();
    const s3Client = getS3Client();
    const body = await readBody(event);
    
    if (!body.key) {
      return createError({
        statusCode: 400,
        message: 'File key is required'
      });
    }
    
    const command = new GetObjectCommand({
      Bucket: config.rabata.bucketName,
      Key: body.key
    });
    
    const signedUrl = await getSignedUrl(s3Client, command, { 
      expiresIn: body.expiresIn || 3600 // Default 1 hour
    });
    
    return { signedUrl };
  } catch (error) {
    console.error('Error generating signed URL:', error);
    return createError({
      statusCode: 500,
      message: 'Error generating signed URL'
    });
  }
});