Express.js Quickstart

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

Introduction

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

Prerequisites

Before you begin, make sure you have:

Installation

Create a New Express.js Application

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

$ mkdir my-express-app
$ cd my-express-app
$ npm init -y
$ npm install express

Install Required Dependencies

Install the AWS SDK for JavaScript and other necessary packages:

$ npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer dotenv uuid

Project Structure

Create a basic project structure:

my-express-app/
├── config/
│   └── s3.js
├── routes/
│   └── upload.js
├── public/
│   └── index.html
├── .env
├── app.js
└── package.json

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
PORT=3000

Create S3 Configuration

Create a configuration file for the S3 client:

// config/s3.js
const { S3Client } = require('@aws-sdk/client-s3');
require('dotenv').config();

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

module.exports = { s3Client };

Set Up Express Application

Create the main application file:

// app.js
const express = require('express');
const path = require('path');
const uploadRoutes = require('./routes/upload');
require('dotenv').config();

const app = express();
const port = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api', uploadRoutes);

// Serve the HTML file
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Start the server
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

Basic Usage

Create Upload Routes

Create a file for upload routes:

// routes/upload.js
const express = require('express');
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const { 
  PutObjectCommand, 
  GetObjectCommand, 
  ListObjectsV2Command,
  DeleteObjectCommand 
} = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { s3Client } = require('../config/s3');

const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });

// Upload a file
router.post('/upload', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }

    const file = req.file;
    const fileName = `${uuidv4()}-${file.originalname}`;

    // Upload to Rabata.io
    await s3Client.send(
      new PutObjectCommand({
        Bucket: process.env.RABATA_BUCKET_NAME,
        Key: fileName,
        Body: file.buffer,
        ContentType: file.mimetype
      })
    );

    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);
    res.status(500).json({ error: 'Error uploading file' });
  }
});

// List all files
router.get('/files', async (req, res) => {
  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}`
    })) || [];

    res.status(200).json({ files });
  } catch (error) {
    console.error('Error listing files:', error);
    res.status(500).json({ error: 'Error listing files' });
  }
});

// Delete a file
router.delete('/files/:key', async (req, res) => {
  try {
    const key = req.params.key;

    await s3Client.send(
      new DeleteObjectCommand({
        Bucket: process.env.RABATA_BUCKET_NAME,
        Key: key
      })
    );

    res.status(200).json({ message: 'File deleted successfully' });
  } catch (error) {
    console.error('Error deleting file:', error);
    res.status(500).json({ error: 'Error deleting file' });
  }
});

// Generate a presigned URL for downloading a file
router.get('/files/:key/url', async (req, res) => {
  try {
    const key = req.params.key;
    const expiresIn = parseInt(req.query.expiresIn) || 3600; // Default 1 hour

    const command = new GetObjectCommand({
      Bucket: process.env.RABATA_BUCKET_NAME,
      Key: key
    });

    const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });

    res.status(200).json({ signedUrl });
  } catch (error) {
    console.error('Error generating signed URL:', error);
    res.status(500).json({ error: 'Error generating signed URL' });
  }
});

module.exports = router;

Create a Simple HTML Interface

Create a simple HTML interface for file uploads:

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Express.js with Rabata.io</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    h1 {
      text-align: center;
    }
    .upload-container {
      border: 1px solid #ddd;
      padding: 20px;
      border-radius: 5px;
      margin-bottom: 20px;
    }
    .form-group {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
    }
    .error {
      color: red;
      margin: 10px 0;
    }
    .success {
      color: green;
      margin: 10px 0;
    }
    .file-list {
      margin-top: 30px;
    }
    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;
    }
    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>
</head>
<body>
  <h1>Express.js with Rabata.io</h1>
  
  <div class="upload-container">
    <h2>Upload File to Rabata.io</h2>
    
    <form id="uploadForm">
      <div class="form-group">
        <label for="file">Select File</label>
        <input type="file" id="file" name="file">
      </div>
      
      <div id="error" class="error" style="display: none;"></div>
      <div id="success" class="success" style="display: none;"></div>
      
      <button type="submit" id="uploadButton">Upload</button>
    </form>
  </div>
  
  <div class="file-list">
    <h2>Files in Rabata.io Bucket</h2>
    <div id="loading">Loading files...</div>
    <ul id="filesList"></ul>
  </div>
  
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const uploadForm = document.getElementById('uploadForm');
      const fileInput = document.getElementById('file');
      const uploadButton = document.getElementById('uploadButton');
      const errorDiv = document.getElementById('error');
      const successDiv = document.getElementById('success');
      const filesList = document.getElementById('filesList');
      const loadingDiv = document.getElementById('loading');
      
      // Fetch files on page load
      fetchFiles();
      
      // Handle form submission
      uploadForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        
        if (!fileInput.files || !fileInput.files[0]) {
          showError('Please select a file');
          return;
        }
        
        const file = fileInput.files[0];
        const formData = new FormData();
        formData.append('file', file);
        
        uploadButton.disabled = true;
        errorDiv.style.display = 'none';
        successDiv.style.display = 'none';
        
        try {
          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');
          }
          
          showSuccess(`File uploaded successfully! <a href="${data.fileUrl}" target="_blank">View File</a>`);
          fileInput.value = '';
          
          // Refresh the file list
          fetchFiles();
        } catch (error) {
          showError(error.message);
        } finally {
          uploadButton.disabled = false;
        }
      });
      
      // Fetch files from the server
      async function fetchFiles() {
        loadingDiv.style.display = 'block';
        filesList.innerHTML = '';
        
        try {
          const response = await fetch('/api/files');
          const data = await response.json();
          
          if (!response.ok) {
            throw new Error(data.error || 'Error fetching files');
          }
          
          loadingDiv.style.display = 'none';
          
          if (data.files.length === 0) {
            filesList.innerHTML = '<p>No files found in the bucket.</p>';
            return;
          }
          
          data.files.forEach(file => {
            const li = document.createElement('li');
            
            const link = document.createElement('a');
            link.href = file.url;
            link.target = '_blank';
            link.textContent = file.key;
            
            const size = document.createElement('span');
            size.className = 'file-size';
            size.textContent = `${(file.size / 1024).toFixed(2)} KB`;
            
            const deleteButton = document.createElement('button');
            deleteButton.className = 'delete-button';
            deleteButton.textContent = 'Delete';
            deleteButton.onclick = () => deleteFile(file.key);
            
            li.appendChild(link);
            li.appendChild(size);
            li.appendChild(deleteButton);
            
            filesList.appendChild(li);
          });
        } catch (error) {
          loadingDiv.style.display = 'none';
          filesList.innerHTML = `<p class="error">Error: ${error.message}</p>`;
        }
      }
      
      // Delete a file
      async function deleteFile(key) {
        if (!confirm(`Are you sure you want to delete ${key}?`)) {
          return;
        }
        
        try {
          const response = await fetch(`/api/files/${encodeURIComponent(key)}`, {
            method: 'DELETE'
          });
          
          const data = await response.json();
          
          if (!response.ok) {
            throw new Error(data.error || 'Error deleting file');
          }
          
          // Refresh the file list
          fetchFiles();
        } catch (error) {
          alert(`Error: ${error.message}`);
        }
      }
      
      // Show error message
      function showError(message) {
        errorDiv.textContent = message;
        errorDiv.style.display = 'block';
        successDiv.style.display = 'none';
      }
      
      // Show success message
      function showSuccess(message) {
        successDiv.innerHTML = message;
        successDiv.style.display = 'block';
        errorDiv.style.display = 'none';
      }
    });
  </script>
</body>
</html>

Advanced Usage

Generating Presigned URLs for Direct Uploads

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

// Add this route to routes/upload.js
router.post('/presigned-upload', async (req, res) => {
  try {
    const { fileName, fileType } = req.body;
    
    if (!fileName || !fileType) {
      return res.status(400).json({ error: 'File name and type are required' });
    }
    
    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 });
    
    res.status(200).json({ 
      presignedUrl,
      key,
      fileUrl: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${key}`
    });
  } catch (error) {
    console.error('Error generating presigned URL:', error);
    res.status(500).json({ error: 'Error generating presigned URL' });
  }
});

Handling Multipart Uploads

For very large files, you can implement multipart uploads:

// Add these routes to routes/upload.js
const { 
  CreateMultipartUploadCommand,
  UploadPartCommand,
  CompleteMultipartUploadCommand,
  AbortMultipartUploadCommand
} = require('@aws-sdk/client-s3');

// Initiate multipart upload
router.post('/multipart/initiate', async (req, res) => {
  try {
    const { fileName, fileType } = req.body;
    
    if (!fileName || !fileType) {
      return res.status(400).json({ error: 'File name and type are required' });
    }
    
    const key = `${uuidv4()}-${fileName}`;
    
    const command = new CreateMultipartUploadCommand({
      Bucket: process.env.RABATA_BUCKET_NAME,
      Key: key,
      ContentType: fileType
    });
    
    const response = await s3Client.send(command);
    
    res.status(200).json({
      uploadId: response.UploadId,
      key: key
    });
  } catch (error) {
    console.error('Error initiating multipart upload:', error);
    res.status(500).json({ error: 'Error initiating multipart upload' });
  }
});

// Get presigned URL for a part
router.post('/multipart/presigned-part', async (req, res) => {
  try {
    const { key, uploadId, partNumber } = req.body;
    
    if (!key || !uploadId || !partNumber) {
      return res.status(400).json({ error: 'Key, uploadId, and partNumber are required' });
    }
    
    const command = new UploadPartCommand({
      Bucket: process.env.RABATA_BUCKET_NAME,
      Key: key,
      UploadId: uploadId,
      PartNumber: parseInt(partNumber)
    });
    
    const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
    
    res.status(200).json({ presignedUrl });
  } catch (error) {
    console.error('Error generating presigned URL for part:', error);
    res.status(500).json({ error: 'Error generating presigned URL for part' });
  }
});

// Complete multipart upload
router.post('/multipart/complete', async (req, res) => {
  try {
    const { key, uploadId, parts } = req.body;
    
    if (!key || !uploadId || !parts || !Array.isArray(parts)) {
      return res.status(400).json({ error: 'Key, uploadId, and parts array are required' });
    }
    
    const command = new CompleteMultipartUploadCommand({
      Bucket: process.env.RABATA_BUCKET_NAME,
      Key: key,
      UploadId: uploadId,
      MultipartUpload: {
        Parts: parts
      }
    });
    
    await s3Client.send(command);
    
    res.status(200).json({
      message: 'Multipart upload completed successfully',
      fileUrl: `${process.env.RABATA_ENDPOINT}/${process.env.RABATA_BUCKET_NAME}/${key}`
    });
  } catch (error) {
    console.error('Error completing multipart upload:', error);
    res.status(500).json({ error: 'Error completing multipart upload' });
  }
});

// Abort multipart upload
router.post('/multipart/abort', async (req, res) => {
  try {
    const { key, uploadId } = req.body;
    
    if (!key || !uploadId) {
      return res.status(400).json({ error: 'Key and uploadId are required' });
    }
    
    const command = new AbortMultipartUploadCommand({
      Bucket: process.env.RABATA_BUCKET_NAME,
      Key: key,
      UploadId: uploadId
    });
    
    await s3Client.send(command);
    
    res.status(200).json({ message: 'Multipart upload aborted successfully' });
  } catch (error) {
    console.error('Error aborting multipart upload:', error);
    res.status(500).json({ error: 'Error aborting multipart upload' });
  }
});

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

Error Handling and Middleware

Create Error Handling Middleware

Create a middleware for handling errors:

// Add this to app.js
// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Something went wrong!',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined
  });
});

File Type Validation Middleware

Create a middleware for validating file types:

// Add this to routes/upload.js
const fileTypeValidation = (allowedTypes) => {
  return (req, res, next) => {
    if (!req.file) {
      return next();
    }
    
    if (!allowedTypes.includes(req.file.mimetype)) {
      return res.status(400).json({
        error: 'Invalid file type',
        message: `Allowed types: ${allowedTypes.join(', ')}`
      });
    }
    
    next();
  };
};

// Usage example
router.post(
  '/upload/images',
  upload.single('file'),
  fileTypeValidation(['image/jpeg', 'image/png', 'image/gif']),
  async (req, res) => {
    // Handle image upload
  }
);

Production Considerations

Security Best Practices

  1. Validate File Types: Always validate uploaded file types to prevent security issues.

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

// Modify the multer configuration
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});
  1. Use Helmet for Security Headers:
$ npm install helmet
// Add to app.js
const helmet = require('helmet');
app.use(helmet());
  1. Rate Limiting:
$ npm install express-rate-limit
// Add to app.js
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/api/', apiLimiter);

Logging

Implement proper logging for production:

$ npm install winston
// Create a logger.js file
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'express-rabata' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

module.exports = logger;

Complete Example

Here’s a complete example of an Express.js application that uses Rabata.io for file storage:

// app.js
const express = require('express');
const path = require('path');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const uploadRoutes = require('./routes/upload');
const logger = require('./logger');
require('dotenv').config();

const app = express();
const port = process.env.PORT || 3000;

// Security middleware
app.use(helmet());

// Rate limiting
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', apiLimiter);

// Basic middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api', uploadRoutes);

// Serve the HTML file
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// Error handling middleware
app.use((err, req, res, next) => {
  logger.error(err.stack);
  res.status(500).json({
    error: 'Something went wrong!',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined
  });
});

// Start the server
app.listen(port, () => {
  logger.info(`Server running on http://localhost:${port}`);
});

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 installed the dotenv package.

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

Debugging Tips

  1. Enable Debug Logging:
// Add to routes/upload.js
const debugLog = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
};

router.use(debugLog);
  1. Check Request and Response:
// Add to app.js
app.use((req, res, next) => {
  const originalSend = res.send;
  res.send = function(data) {
    console.log(`Response: ${data}`);
    originalSend.call(this, data);
  };
  next();
});