Does Express Work With Cloudflare R2?

Fully CompatibleLast verified: 2026-02-26

Express works seamlessly with Cloudflare R2 via the AWS SDK—treat R2 as an S3-compatible backend and build file upload/download features directly into your Express app.

Quick Facts

Compatibility
full
Setup Difficulty
Easy
Official Integration
No — community maintained
Confidence
high
Minimum Versions
Express: 4.0.0

How Express Works With Cloudflare R2

Express has no native R2 support, but Cloudflare R2 is S3-compatible, so you use the official AWS SDK for JavaScript (@aws-sdk/client-s3) with custom endpoint configuration. Your Express routes handle HTTP requests and delegate file operations to the SDK—uploads write directly to R2, downloads stream from R2 through your Express response. The developer experience is straightforward: initialize an S3 client pointing to your R2 bucket's custom domain, then use familiar S3 methods (PutObject, GetObject) in your route handlers. This approach scales well because R2 egress is free, making it cost-effective for image-heavy applications. You'll typically handle multipart uploads, signed URLs for direct downloads, and deletion operations all within Express middleware or route handlers.

Best Use Cases

User avatar/profile image uploads with automatic cleanup on profile deletion
Document management system where Express routes validate and store PDFs/files in R2
CDN-like image serving with Express generating signed URLs for time-limited access
Backup service that streams database exports to R2 on a schedule via Express jobs

Quick Setup

bash
npm install express @aws-sdk/client-s3 dotenv
javascript
import express from 'express';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const app = express();

const s3Client = new S3Client({
  region: 'auto',
  endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY,
    secretAccessKey: process.env.R2_SECRET_KEY,
  },
  forcePathStyle: true,
});

app.post('/upload', async (req, res) => {
  const filename = `uploads/${Date.now()}-file.txt`;
  const uploadCmd = new PutObjectCommand({
    Bucket: process.env.R2_BUCKET,
    Key: filename,
    Body: req,
  });
  await s3Client.send(uploadCmd);
  res.json({ url: `${process.env.R2_PUBLIC_URL}/${filename}` });
});

app.get('/download/:key', async (req, res) => {
  const url = await getSignedUrl(s3Client, 
    new GetObjectCommand({ Bucket: process.env.R2_BUCKET, Key: req.params.key }),
    { expiresIn: 3600 }
  );
  res.redirect(url);
});

app.listen(3000);

Known Issues & Gotchas

critical

Endpoint URL misconfiguration—using Cloudflare's dashboard URL instead of the S3-compatible endpoint

Fix: Use the format: https://<account-id>.r2.cloudflarestorage.com (or your custom domain) and set forcePathStyle: true in SDK config

warning

Large file uploads timing out or hitting memory limits in Express

Fix: Use multipart upload via @aws-sdk/lib-storage or stream the request body directly to R2 instead of buffering

warning

CORS errors when serving R2 content directly to browsers

Fix: Configure R2 bucket CORS settings or proxy downloads through Express routes

critical

Credential exposure if access keys are hardcoded

Fix: Use environment variables and consider Cloudflare's API tokens or temporary credentials via STS

Alternatives

  • Next.js with Vercel Blob—tighter integration but vendor-locked to Vercel infrastructure
  • Django with boto3 and AWS S3—if you prefer Python backends with similar S3-compatible storage
  • MinIO self-hosted + Express—for on-premise S3-compatible storage without cloud dependency

Resources

Related Compatibility Guides

Explore more compatibility guides