Setting Up Cloudflare R2 with Node.js

Yashraj singh
6 min readNov 29, 2024

--

Do you face issues with file uploading in Node.js?

Does AWS S3 feels too expensive for your small projects?

If so, you’re not alone! I had the same questions when starting my projects. AWS costs can escalate quickly, and while Firebase is an alternative, it often comes with storage limitations and higher costs due to the Google Cloud ecosystem.

Why Choose Cloudflare R2?

Cloudflare R2 offers a developer-friendly and cost-effective way to store files. It removes egress fees (a major AWS drawback) and provides a generous free tier, making it ideal for small projects or personal use.

If you’re ready to integrate R2 into your Node.js application, let’s dive into the setup process.

Prerequisites

Before we start, make sure you have:

  1. Node.js installed on your system.
  2. A Cloudflare account with R2 storage set up.
  3. Basic understanding of REST APIs and file management.

Step-by-Step Guide to Set Up Cloudflare R2 with Node.js

1. Initialize a Node.js Project

Run the following command to create a new Node.js project:

npm init -y

This will generate a package.json file.

2. Install Required Dependencies

Install the following npm packages:

  • @aws-sdk/client-s3: To interact with Cloudflare R2, which uses the S3 API.
  • cors: To handle Cross-Origin Resource Sharing for your APIs.
  • multer: To manage file uploads.
npm install @aws-sdk/client-s3 cors multer

3. Set Up Your Node.js Application

Create a file named index.js and use the code snippet below to configure your app for file uploads:

const express = require("express");
const bodyParser = require("body-parser");
const dotenv = require("dotenv");
const cors = require("cors");
const path = require("path");
const multer = require("multer");
const {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
GetObjectCommand,
ListObjectsV2Command,
} = require("@aws-sdk/client-s3");
dotenv.config();

const app = express();
app.use(bodyParser.json());
app.use(cors());

const s3 = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});

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

const BUCKET_NAME = process.env.R2_BUCKET_NAME;

// Helper to convert S3 streams to strings
const streamToString = async (stream) => {
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf-8");
};

app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "templates/index.html"));
});

// 1. Create a file inside a folder
app.post("/create-file", async (req, res) => {
const { folder, fileName, content } = req.body;

const params = {
Bucket: BUCKET_NAME,
Key: `${folder}/${fileName}`,
Body: JSON.stringify(content),
ContentType: "application/json",
};

try {
const command = new PutObjectCommand(params);
await s3.send(command);
res.status(201).send({ message: "File created successfully" });
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 2. Delete a file inside a folder
app.delete("/delete-file", async (req, res) => {
const { folder, fileName } = req.body;

const params = {
Bucket: BUCKET_NAME,
Key: `${folder}/${fileName}`,
};

try {
const command = new DeleteObjectCommand(params);
await s3.send(command);
res.status(200).send({ message: "File deleted successfully" });
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 3. Update a file inside a folder (overwrite)
app.put("/update-file", async (req, res) => {
const { folder, fileName, content } = req.body;

const params = {
Bucket: BUCKET_NAME,
Key: `${folder}/${fileName}`,
Body: JSON.stringify(content),
ContentType: "application/json",
};

try {
const command = new PutObjectCommand(params);
await s3.send(command);
res.status(200).send({ message: "File updated successfully" });
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 4. Read a file inside a folder
app.get("/read-file", async (req, res) => {
const { folder, fileName } = req.query;

const params = {
Bucket: BUCKET_NAME,
Key: `${folder}/${fileName}`,
};

try {
const command = new GetObjectCommand(params);
const data = await s3.send(command);
const content = await streamToString(data.Body);
res.status(200).send(JSON.parse(content));
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 5. List files inside a folder
app.get("/list-files", async (req, res) => {
const { folder } = req.query;

const params = {
Bucket: BUCKET_NAME,
Prefix: `${folder}/`,
};

try {
const command = new ListObjectsV2Command(params);
const data = await s3.send(command);

const files = data.Contents?.map((item) => item.Key) || [];
res.status(200).send(files);
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 6. List all folders
app.get("/list-folders", async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Delimiter: "/",
};

try {
const command = new ListObjectsV2Command(params);
const data = await s3.send(command);

const folders =
data.CommonPrefixes?.map((prefix) => prefix.Prefix) || [];
res.status(200).send(folders);
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 7. Duplicate a folder
app.post("/duplicate-folder", async (req, res) => {
const { sourceFolder, targetFolder } = req.body;

const listParams = {
Bucket: BUCKET_NAME,
Prefix: `${sourceFolder}/`,
};

try {
// List all objects in the source folder
const listCommand = new ListObjectsV2Command(listParams);
const listData = await s3.send(listCommand);

if (!listData.Contents || listData.Contents.length === 0) {
return res
.status(404)
.send({ message: "Source folder is empty or not found" });
}

// Copy each object to the target folder
const copyPromises = listData.Contents.map(async (item) => {
const copyParams = {
Bucket: BUCKET_NAME,
CopySource: `${BUCKET_NAME}/${item.Key}`,
Key: item.Key.replace(sourceFolder, targetFolder),
};
const copyCommand = new PutObjectCommand(copyParams);
return s3.send(copyCommand);
});

await Promise.all(copyPromises);
res.status(201).send({ message: "Folder duplicated successfully" });
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 8. Rename a folder
app.put("/rename-folder", async (req, res) => {
const { sourceFolder, targetFolder } = req.body;

const listParams = {
Bucket: BUCKET_NAME,
Prefix: `${sourceFolder}/`,
};

try {
// List all objects in the source folder
const listCommand = new ListObjectsV2Command(listParams);
const listData = await s3.send(listCommand);

if (!listData.Contents || listData.Contents.length === 0) {
return res
.status(404)
.send({ message: "Source folder is empty or not found" });
}

// Copy each object to the target folder and delete original
const copyAndDeletePromises = listData.Contents.map(async (item) => {
const newKey = item.Key.replace(sourceFolder, targetFolder);

// Copy object to new location
const copyParams = {
Bucket: BUCKET_NAME,
CopySource: `${BUCKET_NAME}/${item.Key}`,
Key: newKey,
};
const copyCommand = new PutObjectCommand(copyParams);
await s3.send(copyCommand);

// Delete original object
const deleteParams = {
Bucket: BUCKET_NAME,
Key: item.Key,
};
const deleteCommand = new DeleteObjectCommand(deleteParams);
return s3.send(deleteCommand);
});

await Promise.all(copyAndDeletePromises);
res.status(200).send({ message: "Folder renamed successfully" });
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// 9. Upload media file
app.post("/upload-files", upload.array("files", 10), async (req, res) => {
const folder = req.body.folder || "uploads";
const files = req.files;

if (!files || files.length === 0) {
return res.status(400).send({ message: "No files uploaded" });
}

try {
const uploadPromises = files.map((file) => {
const params = {
Bucket: BUCKET_NAME,
Key: `${folder}/${file.originalname}`,
Body: file.buffer,
ContentType: file.mimetype,
};

const command = new PutObjectCommand(params);
return s3.send(command);
});

await Promise.all(uploadPromises);
res.status(201).send({
message: "Files uploaded successfully",
fileNames: files.map((file) => file.originalname),
});
} catch (error) {
res.status(500).send({ error: error.message });
}
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

4. Set Up Environment Variables

Create a .env file in your project directory to securely store your Cloudflare R2 credentials:

PORT=3000
AWS_ACCESS_KEY_ID=<your-cloudflare-r2-access-key>
AWS_SECRET_ACCESS_KEY=<your-cloudflare-r2-secret-key>
R2_BUCKET_NAME=<your-cloudflare-r2-bucket-name>
R2_BUCKET=<your-cloudflare-r2-bucket-name>
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com

Replace the placeholders with your Cloudflare R2 account details.

5. Test the API

You can use Postman or a similar API testing tool to test the /upload endpoint. Upload a file by sending a POST request with the file attached as form-data.

Resources

If you’d like a complete implementation, check out my GitHub repository:
GitHub — cloudflare-r2-file-manager

This repository includes a fully functional API for managing files and folders in Cloudflare R2.

Postman Collection

If you’re not familiar with Node.js, you can use the provided Postman collection to test the APIs in any language of your choice.
Postman collection Gist

Conclusion

Setting up Cloudflare R2 with Node.js is straightforward and offers a cost-effective way to handle file storage in your projects. Whether you’re a student, a hobbyist, or a professional, R2 provides flexibility and affordability without sacrificing performance.

Feel free to explore the GitHub repository and Postman collection to jumpstart your development process. With Cloudflare R2, managing file uploads has never been easier!

--

--

Yashraj singh
Yashraj singh

Written by Yashraj singh

Learner | Developer | Software Engineer

No responses yet