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

```bash
$ 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:

```bash
$ 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=us-east-1
RABATA_ENDPOINT=https://s3.us-east-1.rabata.io
```

### Configure Nuxt Runtime Config

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

```typescript
// 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 || 'us-east-1',
      endpoint: process.env.RABATA_ENDPOINT || 'https://s3.us-east-1.rabata.io'
    },
    // Public variables that are exposed to the client
    public: {
      rabataEndpoint: process.env.RABATA_ENDPOINT || 'https://s3.us-east-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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

```vue
<!-- 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">{% raw %}{{ error }}{% endraw %}</p>

      <button type="submit" :disabled="!file || uploading">
        {% raw %}{{ uploading ? 'Uploading...' : 'Upload' }}{% endraw %}
      </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:

```vue
<!-- 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: {% raw %}{{ error }}{% endraw %}</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">
          {% raw %}{{ file.key }}{% endraw %}
        </a>
        <span class="file-size">
          {% raw %}{{ (file.size / 1024).toFixed(2) }}{% endraw %} KB
        </span>
        <button
          class="delete-button"
          @click="handleDelete(file.key)"
          :disabled="deleting === file.key"
        >
          {% raw %}{{ deleting === file.key ? 'Deleting...' : 'Delete' }}{% endraw %}
        </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:

```typescript
// 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:

```vue
<!-- 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:

```typescript
// 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:

```vue
<!-- 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">{% raw %}{{ error }}{% endraw %}</p>

      <div v-if="progress > 0" class="progress-container">
        <div class="progress-bar" :style="{ width: `${progress}%` }"></div>
        <span>{% raw %}{{ progress }}{% endraw %}%</span>
      </div>

      <button
        @click="uploadDirectly"
        :disabled="!selectedFile || uploading"
        class="upload-button"
      >
        {% raw %}{{ uploading ? 'Uploading...' : 'Upload Directly' }}{% endraw %}
      </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:

```json
[
  {
    "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:

```typescript
// 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:

```typescript
// 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'
  });
}
```

2. **Set File Size Limits**: Limit the size of uploaded files:

```typescript
// 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)'
  });
}
```

3. **Use Signed URLs with Expiration**: For private files, use signed URLs with expiration:

```typescript
// 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'
    });
  }
});
```

<div class="docs-article__actions">
  <a href="/docs/quickstart/node/" class="btn btn--secondary">Node.js Quickstart</a>
  <a href="/docs/quickstart/nextjs/" class="btn btn--secondary">Next.js Integration</a>
  <a href="/docs/quickstart/express/" class="btn btn--secondary">Express Integration</a>
</div>
