How to Connect Cloudflare R2 Storage to a Next.js App (Complete Beginner Guide)

Why I Chose Cloudflare R2
Recently, I was building a simple e-commerce application called ShopSphere. The application needed to store:
Product images
Category banners
User-uploaded documents
Initially, I considered several approaches:
Storing files inside the project repository
Supabase Storage
AWS S3
Cloudflare R2
I quickly ruled out storing images in the Git repository. User-generated files are data, not source code. Keeping uploads inside your repository can quickly bloat deployments and make version control difficult.
I wanted a solution that offered:
β Generous free tier
β No surprise bandwidth costs
β Easy integration with Next.js
β Ability to scale later
Cloudflare R2 ended up being an excellent fit.
What is Cloudflare R2?
Cloudflare R2 is an object storage service similar to Amazon S3.
Instead of storing files on your server, files are stored inside buckets and accessed via URLs.
Examples:
Product images
User profile photos
PDF documents
Videos
Application assets
Architecture
My setup looks like this:
Next.js Application
β
API Route / Server Action
β
Cloudflare R2
β
Store File URL in Database
Example database schema:
model Product {
id String @id @default(cuid())
name String
imageUrl String
}
Instead of storing the actual file inside the database, we only store the URL.
Step 1: Create a Cloudflare Account
Visit:
Create a free account and log in.
Step 2: Open R2 Object Storage
From the dashboard:
Storage & Databases β R2 Object Storage
Click:
Create Bucket
Step 3: Create Your First Bucket
I named mine:
shopsphere-assets
Location:
Automatic
Then click:
Create Bucket
Congratulations! Your storage bucket is ready.
Step 4: Understanding "Folders" in R2
This was one of the first things that confused me.
You do NOT create folders manually.
Cloudflare R2 doesn't actually have folders.
It stores objects using keys.
For example:
products/abc123/product-1.webp
products/abc123/product-2.webp
products/xyz456/product-1.webp
Cloudflare automatically displays this as:
products/
βββ abc123/
β βββ product-1.webp
β βββ product-2.webp
βββ xyz456/
βββ product-1.webp
These folders don't physically exist.
They're simply prefixes inside object names.
Recommended Object Structure
Avoid using names or slugs because they can change.
Instead:
products/
βββ product-id/
β βββ image-1.webp
β βββ image-2.webp
β βββ image-3.webp
Example:
products/cmgx123/image-1.webp
This approach scales much better.
Step 5: Creating API Credentials
Navigate to:
R2 Object Storage β Overview β Manage API Tokens
At this point, you'll see two options:
Option 1: Account API Tokens
These tokens belong to the Cloudflare account itself.
Examples:
Production applications
Server-side applications
Shared infrastructure
Team environments
Benefits:
β Recommended by Cloudflare for production
β Continues working even if team members change
β Better suited for long-term projects
Option 2: User API Tokens
These tokens belong to your personal user account.
Examples:
Local development
Personal experiments
Temporary scripts
Benefits:
β Quick to create
β Good for testing
Drawbacks:
β Tied to your user account
β Less ideal for production systems
Which One Should You Choose?
For a real application, I recommend:
Account API Token
For learning or quick experiments:
User API Token
I initially found this screen confusing because both options looked very similar.
For my application, I chose:
Create Account API Token
Step 6: Configure the Token
Token Name:
ShopSphere Production
Permissions:
Object Read & Write
Bucket Access:
Specific Bucket
Choose:
shopsphere-assets
TTL:
Forever
Then click:
Create Account API Token
Cloudflare will generate:
Account ID
Access Key ID
Secret Access Key
Endpoint
β οΈ Important
The Secret Access Key is shown only once.
Copy it immediately and store it somewhere safe.
Step 7: Add Environment Variables
Inside Netlify:
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=shopsphere-assets
R2_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
Step 8: Install the SDK
Cloudflare R2 uses an S3-compatible API.
Install the AWS SDK:
npm install @aws-sdk/client-s3
No Cloudflare-specific SDK is required.
Step 9: Configure the R2 Client
Create:
lib/r2.ts
import { S3Client } from "@aws-sdk/client-s3";
export const r2 = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey:
process.env.R2_SECRET_ACCESS_KEY!,
},
});
Step 10: Upload Flow
A typical upload process looks like this:
User Upload
β
Validate file
β
Resize image
β
Convert to WebP
β
Upload to R2
β
Store URL in Database
What About Public URLs?
This part also confused me initially.
Depending on when you're reading this, Cloudflare's dashboard may look slightly different and you might not immediately see public access settings.
The good news is:
You don't need public URLs immediately.
For an MVP, this setup works perfectly:
Browser
β
Next.js API Route
β
Cloudflare R2
Example:
https://myapp.netlify.app/api/images/products/cmgx123/image-1.webp
Your API route:
Reads the file from R2
Streams it back
Browser displays the image
Benefits:
β No custom domain required
β Bucket can remain private
β Easy to add authentication
β Better control over access
Later: Add a Custom Domain
Eventually, you may want something cleaner:
cdn.myapp.com
Your URLs become:
https://cdn.myapp.com/products/cmgx123/image-1.webp
The nice part?
You don't need to move any files.
Only the URL changes.
Recommended Setup for Small Projects
Phase 1
Database
Next.js Application
Private R2 Bucket
API Route
Phase 2
Purchase Domain
Phase 3
Custom CDN Domain
Public R2
Cloudflare CDN
Final Thoughts
Cloudflare R2 turned out to be surprisingly easy to work with.
It gives you:
Generous free storage
No egress fees
S3-compatible APIs
Excellent scalability
Simple integration with Next.js
If you're building an application that stores images, documents, or user uploads, Cloudflare R2 is definitely worth considering.





