NextJS on Cloudflare Wiki

Published on

Initialize, write and deploy NextJS applications optimized for running on Cloudflare.

Platforma
  • Next.js is an open-source web development framework created by the private company Vercel providing React-based web applications with server-side rendering and static rendering, see more at Next.JS page.
  • Cloudflare is one of the biggest network service for the purposes of increasing the security and performance of web sites and services. It also provides a cloud service to host files and applications.

2 Options

  1. Initialize a NextJs app and set it for Cloudflare:
    npx create-next-app@latest <appName>

    You can follow options from https://vrealmatic.com/typescript/nextjs.

  2. Add output: "export", trailingSlash: true, to next.config.ts. The file may look as follow:
    import type { NextConfig } from "next";
    
    const nextConfig: NextConfig = {
      output: "export",
      trailingSlash: true,
    };
    
    export default nextConfig;
  3. Add wrangler.jsonc file to root
    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "next444",
      "main": "src/worker.ts",
      "compatibility_date": "2026-03-04",
      "assets": {
        "directory": "./out",
        "binding": "ASSETS",
        "run_worker_first": ["/api/*"]
      }
    }
  4. Add small worker src/worker.ts
    interface AssetsBinding {
      fetch(input: Request | URL | string, init?: RequestInit): Promise<Response>;
    }
    
    interface Env {
      ASSETS: AssetsBinding;
    }
    
    export default {
      async fetch(request: Request, env: Env): Promise<Response> {
        const url = new URL(request.url);
    
        if (url.pathname === "/api/hello") {
          return Response.json({
            ok: true,
            message: "Hello from Worker API",
          });
        }
    
        return env.ASSETS.fetch(request);
      },
    };
  5. Install Wrangler
    npm install -D wrangler

Preview:

npx wrangler dev

Deploy:

npx wrangler deploy

Creating D1 database and connecting it

Either throgh Cloudflare UI, or through wrangler in terminal - In project root folder terminal, run

npx wrangler d1 create my-next-app-db

D1 Typing

npx wrangler types --env-interface CloudflareEnv

Connect DB

  • In wrangler.jsonc, add
    "d1_databases": [
            {
                "binding": "my_db",
                "database_name": "my-next-app-db",
                "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "remote": true
            }
        ]
  • Then in any script you need the db, or in a lib
    import { getCloudflareContext } from "@opennextjs/cloudflare";
    
    type Env = { my_db: D1Database; // binding name in wrangler.jsonc };
    
    export function getD1() {
      const { env } = getCloudflareContext() as unknown as { env: Env };
      const db = env.my_db;
    }

Integration R2 Storage Database to NextJS application

R2 Storage Database can be configured in CloudFlare UI: Storage & Databases / R2 Object Storage. Alternatively, you can create R2 storage database also through cli, see Create new buckets.

R2 Storage can be used for holding any type of files, from common web media up to cache jsons.

Config wrangler.jsonc

"r2_buckets": [
    {
        "binding": "NEXT_INC_CACHE_R2_BUCKET",
        "bucket_name": "cache"
    }
],

Sample R2 Library src/lib/r2.ts

import { getCloudflareContext } from "@opennextjs/cloudflare";

type Env = { NEXT_INC_CACHE_R2_BUCKET: R2Bucket; };
   
const r2CacheKey : string = 'vrealmatic_page/v3/';
       
async function getBucket(): Promise<R2Bucket> {
    const ctx = getCloudflareContext() as { env: Env };
    const bucket = ctx.env.NEXT_INC_CACHE_R2_BUCKET;
    if (!bucket) throw new Error("R2 binding NEXT_INC_CACHE_R2_BUCKET is missing in Cloudflare context env.");
    return bucket;
}
            
export async function r2GetJson<T>(key: string): Promise<T | null> {
    const bucket = await getBucket();
    const obj = await bucket.get(r2CacheKey+key);
    if (!obj) return null;
    return await obj.json<T>();
}
            
export async function r2PutJson(key: string, value: unknown): Promise<void> {
    const bucket = await getBucket();
    await bucket.put((r2CacheKey+key), JSON.stringify(value), { httpMetadata: { contentType: "application/json" } });
}

Use Sample for saving cache data to R2

type R2Cached<T> = {
    ts: number;     // stored at (ms)
    data: T;
};
                    
const PAGE_CTX_TTL_MS = 86_400_000; // 24h
                    
const inFlight = new Map<string, Promise<PageContext>>();
async function getRequestedAndAlternates_TTL(urlPath: string): Promise<PageContext> {
                    
    const existing = inFlight.get(urlPath);
    if (existing) return existing;
                      
    const p = (async () => {
        try {
          const now = Date.now();
                    
          // 1) try cache
          const cached = await r2GetJson<R2Cached<PageContext>>(urlPath);
          if ( cached && (now - cached.ts) < PAGE_CTX_TTL_MS ) return cached.data;
          
          // 2) MISS/STALE => DB
          const data: PageContext = await GetPageByUrlAndAlternates(urlPath);
                          
          // 3) Write / update to cache, if the page exists
          if(data.page) await r2PutJson(urlPath, { ts: now, data } satisfies R2Cached<PageContext>);
                    
          return data;
        } finally {
          inFlight.delete(urlPath);
        }
      })();
                    
      inFlight.set(urlPath, p);
      return p;
    }
                    
    export function getPageContext(slug?: string[]) {
      const baseUrlPath = (!slug || slug.length === 0) ? "" : slug.join("/");
      return getPageContextCache(baseUrlPath);
    }
                    
    export const getPageContextCache = cache(getRequestedAndAlternates_TTL);

Incremental cache for NextJS on Cloudflare

  1. At wrangler.jsonc, add bindings to R2 bucket and Durable Object + migration, such as:
    "r2_buckets": [
    		{
    			"binding": "NEXT_INC_CACHE_R2_BUCKET",
    			"bucket_name": "inc-cache"
    		},
    		//...
    	],
    "durable_objects": {
    		"bindings": [
    			{
    				"name": "NEXT_CACHE_DO_QUEUE",
    				"class_name": "DOQueueHandler"
    			}
    		]
    	},
    "migrations": [
    		{ "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"] },
    	],
  2. At open-next.config.ts, attach incremenatl cache configuration, such as:
    import { defineCloudflareConfig } from "@opennextjs/cloudflare";
    import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
    import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
    import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
    
    export default defineCloudflareConfig({
    	// See https://opennext.js.org/cloudflare/caching for more details
    	incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }), 
    	queue: doQueue,
    });
  3. Expand your worker-entry.mjs for DO
    import openNextWorker from "./.open-next/worker.js";
    export { DOQueueHandler } from "./.open-next/worker.js";
    
    export default {
      async fetch(request, env, ctx) {
        const url = new URL(request.url);
    
        if (url.pathname === "/en" || url.pathname === "/en/") {
          url.pathname = "/";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname === "xxx.com") {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname.endsWith(".workers.dev")) {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        return withEdgeCache(request, ctx, (req) => openNextWorker.fetch(req, env, ctx));
      },
    };
    
  4. Build & Deploy

This mechanism uses default doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue". DO is created automatically. There is nothing next to do above the steps described above.

This mechanism eliminates observability errors of type "Failed to revalidate stale page ... FatalError: Dummy queue is not implemented".

Vrealmatic consulting

Anything unclear?

Let us know!

Contact Us