The No-Nonsense Guide to Migrating from Pages to Workers
Ready to level up your Cloudflare project? Our no-nonsense guide walks you through migrating from Pages to Workers for ultimate control and dynamic power.
Daniel Carter
Senior Developer Advocate specializing in serverless architecture and edge computing solutions.
Cloudflare Pages is a dream for deploying static sites. You connect a Git repository, push your code, and—*poof*—your site is live on a global network. It’s simple, fast, and incredibly powerful for what it does. But what happens when your project outgrows "simple"? What if you need more dynamic logic, custom middleware, or fine-grained control over every single request?
That’s when you start eyeing Cloudflare Workers. Workers are the powerhouse of the Cloudflare ecosystem, a serverless platform that lets you run JavaScript (and Wasm) at the edge, intercepting and modifying requests before they even hit an origin server. The move from Pages to Workers can feel like trading in a go-kart for a Formula 1 car—it’s a massive upgrade in power, but it also comes with a steeper learning curve.
This guide is here to flatten that curve. We're cutting through the noise to give you a straightforward, no-nonsense path for migrating your project from Cloudflare Pages to Cloudflare Workers. We'll cover why you’d make the switch, what to prepare, and a step-by-step process to get you across the finish line with confidence.
Why Migrate? Pages vs. Workers Showdown
Before you start moving files around, it’s crucial to understand the fundamental differences. Pages is an opinionated, integrated platform for static sites with light serverless functions. Workers is an unopinionated, code-first environment for... well, almost anything.
Feature | Cloudflare Pages | Cloudflare Workers |
---|---|---|
Primary Use Case | Static site hosting (Jamstack) | Full-stack applications, APIs, middleware, request/response modification |
Configuration | Convention-based (e.g., /functions directory) |
Code-based (wrangler.toml file and your script's entry point) |
Routing | File-system based for assets and functions | Completely custom. You control all routing in your code. |
Static Assets | Handled automatically out-of-the-box | Requires explicit handling (e.g., Workers Sites or a binding to an R2 bucket) |
Middleware | Limited (_middleware.js ) |
Unlimited. The entire Worker is essentially middleware. |
Control | Simple and streamlined | Absolute, granular control over every request and response |
You should migrate if you find yourself needing:
- Complex routing logic that doesn't map neatly to a file system.
- Advanced middleware chains for things like A/B testing, authentication, or custom logging on every request.
- To run a full-stack application entirely on the edge, without a traditional origin server.
- Direct integration with other Cloudflare products like R2, D1, or Queues in a more flexible way.
The Pre-Migration Checklist
A smooth migration is all about preparation. Don't start coding until you've gone through this checklist.
1. Inventory Your Assets
List all your static assets: HTML, CSS, JavaScript bundles, images, fonts, etc. In Pages, these are just files in your project. In a Worker, you'll need to decide how to serve them. Your `public` or `dist` folder is your starting point.
2. Understand Your Functions
Review every file in your /functions
directory. What does each one do? Is it a simple API endpoint? Does it handle form submissions? These will need to be re-written as routes within your single Worker script.
3. Set Up Your Local Environment
You can't migrate without Wrangler, the command-line tool for Cloudflare's developer platform. If you don't have it, install it now:
npm install -g wrangler
You'll also need Node.js and npm installed on your machine.
4. Plan Your Routing
How will you map incoming requests to different logic? For a small project, a simple `if/else` or `switch` statement on the URL pathname might suffice. For anything more complex, consider a lightweight router like `itty-router`, which is designed specifically for Workers.
The Migration Process: A Step-by-Step Guide
Alright, let's get our hands dirty. We'll take this one step at a time.
Step 1: Initialize Your Worker
Create a new directory for your project and run the Wrangler initialization command:
mkdir my-worker-project && cd my-worker-project
wrangler init
Wrangler will create a few files, most importantly:
wrangler.toml
: Your Worker's configuration file. This is where you define its name, compatibility dates, and any bindings to other services.src/index.js
(or.ts
): The entry point for your Worker. All requests will be handled by the `fetch` handler exported from this file.
Step 2: Serving Static Assets (The Right Way)
This is the biggest mindset shift from Pages. Your Worker doesn't automatically serve files. The best way to replicate the Pages experience is with Workers Sites.
1. Create a public
folder in your project root and move all your static assets (from your old Pages project's build output, e.g., `dist` or `build`) into it.
2. Configure your wrangler.toml
to use Workers Sites. Add this section to the file:
[site]
bucket = "./public" # or whatever your asset folder is named
Now, when you run wrangler dev
, Wrangler will automatically upload your static assets and make them available to your Worker. The standard Worker template doesn't include the asset handling logic, so you'll need to import it from `@cloudflare/kv-asset-handler`. Your src/index.js
should look something like this to get started:
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
import manifestJSON from '__STATIC_CONTENT_MANIFEST';
const assetManifest = JSON.parse(manifestJSON);
export default {
async fetch(request, env, ctx) {
try {
// First, try to serve a static asset
return await getAssetFromKV(
{
request,
waitUntil: ctx.waitUntil.bind(ctx),
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: assetManifest,
}
);
} catch (e) {
// If the asset is not found, you can then proceed to your dynamic logic
// For now, let's just return a 404
let pathname = new URL(request.url).pathname;
return new Response(`Asset not found at ${pathname}`, { status: 404 });
}
},
};
This code attempts to find a matching static asset for every request. If it fails (throws an error), you know the request is for a dynamic route, which we'll handle next.
Step 3: Migrating Pages Functions to Worker Logic
Let's say you had a Pages Function at /functions/api/user.js
that returned some JSON. To replicate this, you'll add routing logic inside the `catch` block from the previous step.
Using `itty-router` makes this clean and scalable. First, install it: npm install itty-router
.
Now, modify your src/index.js
to include the router:
import { Router } from 'itty-router';
import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
import manifestJSON from '__STATIC_CONTENT_MANIFEST';
const assetManifest = JSON.parse(manifestJSON);
// Create a new router
const router = Router();
// Our old /api/user route
router.get('/api/user', () => {
const user = { id: 1, name: 'John Doe', email: 'john.doe@example.com' };
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
});
// Catch-all for everything else - a 404
router.all('*', () => new Response('Not Found.', { status: 404 }));
export default {
async fetch(request, env, ctx) {
try {
// Try for a static asset first
return await getAssetFromKV(
{ request, waitUntil: ctx.waitUntil.bind(ctx) },
{ ASSET_NAMESPACE: env.__STATIC_CONTENT, ASSET_MANIFEST: assetManifest }
);
} catch (e) {
// If it's not a static asset, pass it to the router
return router.handle(request);
}
},
};
See how clean that is? You can now easily translate every file from your old /functions
directory into a route in your `router` object.
Step 4: Handling Redirects and Headers
Did you have a _redirects
or _headers
file in your Pages project? These also need to be converted into code.
For redirects: Add a route that returns a Response.redirect()
.
// Replicating a line from _redirects: /old-path /new-path 301
router.get('/old-path', () => Response.redirect('https://your-site.com/new-path', 301));
For headers: You can create a middleware function with your router to add headers to certain paths, or simply add them to your `Response` objects as needed.
// Replicating a rule from _headers for /assets/*
router.get('/assets/*', (request) => {
// In a real app, you would fetch the asset here
// For this example, we assume it's handled by the KV asset handler
// and this route is for adding headers to dynamic content under /assets
const response = new Response('Some asset content');
response.headers.set('X-Custom-Header', 'My-Value');
response.headers.set('Cache-Control', 'max-age=31536000');
return response;
});
Deployment and Post-Migration Sanity Checks
Once you've migrated your assets and logic, it's time to go live.
1. Deploying with Wrangler
The moment of truth. Run the deploy command:
wrangler deploy
Wrangler will bundle your code, upload your static assets, and deploy your Worker to Cloudflare's global network. It will give you a *.workers.dev
URL to test with.
2. Updating Your DNS and Custom Domain
In the Cloudflare dashboard, go to your domain, navigate to the "Workers & Pages" tab, and add a route. For example, you can route your-domain.com/*
to your newly deployed Worker. This tells Cloudflare to send all traffic for your site through the Worker instead of the Pages project. You can then safely disconnect the custom domain from your Pages project.
3. Testing Everything
Don't skip this step! Systematically check:
- Does your homepage load correctly?
- Are all CSS, JS, and images loading? Check the browser's Network tab.
- Do your API endpoints work as expected? Test them with `curl` or a tool like Postman.
- Are your redirects working?
- Check the browser console for any errors.
Conclusion: Was It Worth It?
Migrating from Pages to Workers is an investment. You trade the simplicity of a convention-over-configuration platform for the raw power and flexibility of a code-first environment. The initial setup is more involved, requiring you to manually handle things like static assets and routing that Pages does for you automatically.
But the payoff is immense. Your application is no longer constrained. You have complete control to build whatever you can imagine, right at the edge. By following this guide, you've not only moved your project but also built a solid, scalable foundation for its future. Welcome to the world of limitless possibilities with Cloudflare Workers.