Published on

Setting Up Cloudflare R2 with Node.js

Authors

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.

What is Cloudflare R2?

Cloudflare R2 is a cloud storage service that offers an S3-compatible API with zero egress fees.

Want more cloud storage and Node.js tutorials?

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!