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:
- Node.js 14.x or later installed
- A Rabata.io account with access credentials
- A bucket created in your Rabata.io account
- Basic knowledge of Express.js and Node.js
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
-
Validate File Types: Always validate uploaded file types to prevent security issues.
-
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
}
});
- Use Helmet for Security Headers:
$ npm install helmet
// Add to app.js
const helmet = require('helmet');
app.use(helmet());
- 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
-
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 installed the dotenv package. -
Access Denied Errors: Ensure your Rabata.io credentials are correct and the bucket exists.
Debugging Tips
- 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);
- 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();
});