Without a doubt, Vercel is the ideal platform for deploying a Next.js app. This makes sense, since Vercel created Next.js. And while it offers a generous free plan, things can get expensive quickly as your app grows. Depending on your situation, Vercel may end up being less cost-effective compared to other options.
This tutorial will explore how to build and deploy a Next.js app to Cloudflare Workers. We’ll also cover how to configure GitHub integration for CI/CD and optimize images using Cloudflare Images. With the final setup, you can enjoy Vercel-like performance with more flexibility and lower costs, especially at scale.
To follow along with the tutorial, you’ll need a Cloudflare account. Sign up here if you don’t have one yet.
How to deploy a Next.js app to Cloudflare
For this tutorial, we’ll create a simple product display application that uses a Next.js API route to fetch products from the Fake Store API. Then, on the homepage, we’ll make a request to that API route and display the products in a list.
Create a Next.js app with OpenNext
To get started, run the command below to scaffold a new app using the OpenNext adapter:
npm create cloudflare@latest -- my-next-app --framework=next --platform=workers
This command sets up a Next.js project configured to run on Cloudflare Workers. During setup, you’ll be asked to customize the project like a typical create-next-app flow. However, once the project is created, the following new files will be added by OpenNext:
Wrangler.json
– This file includes configurations for how Cloudflare Workers should deploy your app, such as the deployment name, environment variables, build settings, and the location of your build outputOpennext.config.mjs
– Handles how OpenNext builds and serves your app
To proceed, navigate to pages/api/
directory, create a new file called store.js
, and paste the following code into it:
export default async function handler(req, res) { try { const response = await fetch("https://fakestoreapi.com/products/"); const products = await response.json(); res.status(200).json(products); } catch (error) { res.status(500).json({ error: "Failed to fetch products" }); } }
The code above simply creates an API route that returns product data from fakestoreapi.com.
Next, open the default pages/index.js
file and replace its contents with the following code:
import { useState, useEffect } from "react"; import { Geist } from "next/font/google"; import Image from "next/image"; const geist = Geist({ subsets: ["latin"], variable: "--font-geist", }); export default function Home() { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch("/api/store") .then((res) => res.json()) .then((data) => { setProducts(data); setLoading(false); }); }, []); if (loading) { return (Loading...
); } return (); }{products.map((product) => ())} ${product.price}★ {product.rating.rate} ({product.rating.count})
{product.title}
{product.category}
This code updates the homepage to fetch and display a list of products using the API we just created. Also, since we’re loading images from the Fake Store API, we need to allow that external domain in the images config of Next.js. To do this, update your next.config.mjs
file to match the one below:
/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { domains: ["fakestoreapi.com"], }, }; export default nextConfig; // added by create cloudflare to enable calling `getCloudflareContext()` in `next dev` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; initOpenNextCloudflareForDev();
Now, run the app locally:
npm run dev
You should see a list of fake products on your homepage, as shown in the image below:
Deploy to Cloudflare
As mentioned earlier, the wrangler.json
file lets you customize your app’s deployment settings. You can easily change the app name before deployment by updating the name field. In my case, I’m changing the name to storeapp
, as shown below:
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "storeapp", // Update this to the name of your project "main": ".open-next/worker.js", . . . }
Next, deploy your app by running:
npm run deploy
If you’re running the command for the first time, Cloudflare will prompt you to authenticate. Once you’ve done that, it will build and deploy the app to your Cloudflare Workers account.
In some cases, you might run into a TypeScript build error. You can fix this by installing TypeScript and the required type definitions:
npm install --save-dev typescript @types/react @types/node
After that, try running the deploy command again, and it should work without issues.
Set up GitHub integration
Cloudflare lets you set up GitHub integration to automatically build and deploy your app every time you push changes to your Git repository.
To get started, create a new repo for your project. Once the repo is created, copy the remote URL, then open your project folder locally and run the following commands to initialize Git and push your code:
git init git remote add origingit add . git commit -m "Initial commit" git push -u origin main
Make sure to replace
with the actual URL of your GitHub repository. Once the project is pushed to GitHub, go back to the app you created earlier in your Cloudflare dashboard and navigate to its settings section. Here, under Build & Deploy, you’ll see an option to connect a Git provider:
Select the GitHub option and authorize your GitHub account. Once the authorization is successful, choose the repo you just pushed. Now, every time you push to this repo, Cloudflare will automatically build and deploy your app.
Image optimization
Next.js has a built-in
that automatically optimizes images for faster page loads. You can also pass a custom loader to this component, for example, to generate signed URLs, serve images from a custom CDN, or integrate with services like Cloudflare Images.
To use a custom loader with Cloudflare Images, create a new file called imageLoader.ts
in the root of your project (same level as package.json
) and add the following code:
import type { ImageLoaderProps } from "next/image"; const normalizeSrc = (src: string) => { return src.startsWith("/") ? src.slice(1) : src; }; export default function cloudflareLoader({ src, width, quality, }: ImageLoaderProps) { if (process.env.NODE_ENV === "development") { return src; } const params = [`width=${width}`]; if (quality) { params.push(`quality=${quality}`); } const paramsString = params.join(","); return `/cdn-cgi/image/${paramsString}/${normalizeSrc(src)}`; }
Now, update your next.config.mjs
to register the custom loader:
/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { domains: ["fakestoreapi.com"], loader: "custom", loaderFile: "./imageLoader.ts", }, }; export default nextConfig; // added by create cloudflare to enable calling `getCloudflareContext()` in `next dev` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; initOpenNextCloudflareForDev();
With this update, Next.js will use your custom loader for all images, allowing you to serve optimized images directly from Cloudflare’s CDN, which improves load times and reduces bandwidth costs.
Comparing Cloudflare and Vercel
Beyond pricing, Cloudflare might be a better alternative to Vercel in other scenarios, too. For example, Cloudflare’s edge network and performance at scale can be more robust than what Vercel offers, especially for global audiences or apps that rely heavily on server-side logic at the edge.
The table below also provides a high-level overview to help you decide which platform might suit your needs better:
Feature | Vercel | Cloudflare (Workers + OpenNext) |
---|---|---|
Developer Experience | Excellent with zero-config for Next.js | Good. More setup required, but getting better |
Performance | Great, with built-in edge functions | Top-tier, with global edge by default |
Pricing | Can get expensive quickly (especially for Pro/Team plans or high traffic) | Much cheaper at scale, generous free tier, pay-as-you-go |
Image Optimization | Built-in with |
Needs manual setup (e.g. Cloudflare Images + custom loader) |
Customizability | Limited and mostly within Vercel’s ecosystem | High with full control over routing, caching, edge logic |
Use Case | Best for fast Next.js projects with minimal config | Great for advanced/edge-heavy apps and cost efficiency |
Conclusion
In this article, we explored how to build and deploy a Next.js application to Cloudflare Workers using the OpenNext adapter. We walked through setting up the project, creating a simple API route, enabling image optimization with Cloudflare Images, and configuring GitHub for automatic deployments. You can also find the complete code used in this tutorial on GitHub, and preview the final app deployed on Cloudflare here.
Should you switch from Vercel to Cloudflare? If Vercel is working fine and you value ease of use, you can definitely stick with it. However, if you’re running into cost issues or need more control and edge-level performance, Cloudflare is definitely worth considering.