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:
- Node.js 16.x or later installed
- A Rabata.io account with access credentials
- A bucket created in your Rabata.io account
- Basic knowledge of Nuxt.js and Vue.js
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
-
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
.envfile 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 server API routes to debug issues. -
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
- 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'
});
}
- 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)'
});
}
- 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'
});
}
});