While working on an AI-powered document parser for one of my side projects, I needed a way to let users upload images from their phones or desktops and store them securely in the cloud. At first, I considered using AWS S3, but I wanted something more cost-effective for outbound bandwidth, especially since the files were often accessed publicly. That's when I came across Cloudflare R2.
R2 is a drop-in S3-compatible object storage service that offers zero egress fees, which is ideal for applications that need to serve user-uploaded content. In this guide, I’ll walk you through how I integrated R2 into my Next.js application. The final setup uses presigned URLs for uploads, works well with server components, and is production-ready.
If you're building anything that lets users upload files—like a document scanner, CMS, or file manager—this guide should help you integrate Cloudflare R2 cleanly with your existing Next.js codebase.
Initial Setup
To get started, you need a few key packages. We'll use AWS's official SDK to interact with R2 since R2 is fully S3-compatible. We're also going to use nanoid to generate unique file names and optionally sonner for toast notifications during the upload process.
Once that’s done, go into your Cloudflare dashboard and create a new R2 bucket. After that:
Generate an API token with permission to Read and Write Objects.
Note your Account ID, Access Key ID, and Secret Access Key. You’ll need them soon.
Optionally, set up a custom domain for serving public files from R2, but this is not required for this guide.
Cloudflare dash
R2 Client Configuration
To talk to R2 from your server, we’ll configure an S3 client using AWS SDK. This is where most people run into trouble—especially with region-specific endpoints.
Create a new file: src/lib/r2-client.ts.
This file does three things:
Initializes the S3Client with the correct endpoint and credentials.
Exports configuration like bucket name and upload limits.
Provides utility functions for health checks and error handling.
Here's the key part to understand: R2 has two different endpoint formats, depending on your bucket region. If your R2 bucket is in the EU region, you need to use the .eu.r2.cloudflarestorage.com endpoint. Otherwise, it’s just .r2.cloudflarestorage.com.
The code below sets that automatically based on your environment variables:
Next.js loads environment variables differently on the server and client. You’ll need to set them up carefully to avoid subtle bugs like broken image URLs or failed uploads.
Make sure your .env.example also includes these so other developers don’t miss them. And if you update .env, remember that you need to restart your dev server for changes to take effect.
File Upload Implementation
The upload system has two parts:
An API route that generates a presigned URL for secure uploading.
A React hook that sends the file to that URL and tracks upload progress.
We’re using presigned URLs here, which means the actual file never touches your server. The server just signs the request. This works well in most modern apps and avoids CORS issues if configured correctly.
Create src/app/api/upload/presigned-url/route.ts and define the POST handler. The server will validate the request, generate a file path, and return a one-time upload URL:
Use either a public URL or proxy route to access uploaded files
Conclusion
Cloudflare R2 is a solid choice for storing user-uploaded files, especially when you're looking to avoid egress fees and keep things simple with S3-compatible tooling. Integrating it into a Next.js app takes a bit of careful setup, especially around region-specific endpoints, CORS behavior, and environment variables—but once it's in place, it's fast, reliable, and easy to maintain.
I wrote this guide based on the exact steps I followed when building a document uploader for an AI parser app. If you're building anything similar, I hope this helps you skip the usual gotchas and get straight to a working integration.
If you run into anything unexpected or have improvements, feel free to reach out or suggest changes. I'm always happy to hear how others approach this.