
Cloudflare Workers Migration
- 206 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Use cloudflare-workers-migration for development tasks
About
cloudflare-workers-migration: A skill for development. This provides functionality for development workflows.
- cloudflare-workers-migration
Cloudflare Workers Migration by the numbers
- 206 all-time installs (skills.sh)
- +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,899 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workers-migrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 206 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Use cloudflare-workers-migration for development tasks
Files
Workers Migration Guide
Migrate existing applications to Cloudflare Workers from various platforms.
Migration Decision Tree
What are you migrating from?
├── AWS Lambda
│ └── Node.js handler? → Lambda adapter pattern
│ └── Python? → Consider Python Workers
│ └── Container/custom runtime? → May need rewrite
├── Vercel/Next.js
│ └── API routes? → Minimal changes with adapter
│ └── Full Next.js app? → Use OpenNext adapter
│ └── Middleware? → Direct Workers equivalent
├── Express/Node.js
│ └── Simple API? → Hono (similar API)
│ └── Complex middleware? → Gradual migration
│ └── Heavy node: usage? → Compatibility layer
└── Other Edge (Deno Deploy, Fastly)
└── Standard Web APIs? → Minimal changes
└── Platform-specific? → Targeted rewritesPlatform Comparison
| Feature | Workers | Lambda | Vercel | Express |
|---|---|---|---|---|
| Cold Start | ~0ms | 100-500ms | 10-100ms | N/A |
| CPU Limit | 50ms/10ms | 15 min | 10s | None |
| Memory | 128MB | 10GB | 1GB | System |
| Max Response | 6MB (stream unlimited) | 6MB | 4.5MB | None |
| Global Edge | 300+ PoPs | Regional | ~20 PoPs | Manual |
| Node.js APIs | Partial | Full | Full | Full |
Top 10 Migration Errors
| Error | From | Cause | Solution |
|---|---|---|---|
fs is not defined | Lambda/Express | File system access | Use KV/R2 for storage |
Buffer is not defined | Node.js | Node.js globals | Import from node:buffer |
process.env undefined | All | Env access pattern | Use env parameter |
setTimeout not returning | Lambda | Async patterns | Use ctx.waitUntil() |
require() not found | Express | CommonJS | Convert to ESM imports |
Exceeded CPU time | All | Long computation | Chunk or use DO |
body already consumed | Express | Request body | Clone before read |
Headers not iterable | Lambda | Headers API | Use Headers constructor |
crypto.randomBytes | Node.js | Node crypto | Use crypto.getRandomValues |
Cannot find module | All | Missing polyfill | Check Workers compatibility |
Quick Migration Patterns
AWS Lambda Handler
// Before: AWS Lambda
export const handler = async (event, context) => {
const body = JSON.parse(event.body);
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello' }),
};
};
// After: Cloudflare Workers
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const body = await request.json();
return Response.json({ message: 'Hello' });
},
};Express Middleware
// Before: Express
app.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// After: Hono Middleware
app.use('*', async (c, next) => {
if (!c.req.header('Authorization')) {
return c.json({ error: 'Unauthorized' }, 401);
}
await next();
});Environment Variables
// Before: Node.js
const apiKey = process.env.API_KEY;
// After: Workers
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const apiKey = env.API_KEY;
// ...
},
};Node.js Compatibility
Workers support many Node.js APIs via compatibility flags:
// wrangler.jsonc
{
"compatibility_flags": ["nodejs_compat_v2"],
"compatibility_date": "2024-12-01"
}Supported with nodejs_compat_v2:
crypto(most methods)buffer(Buffer class)util(promisify, types)stream(Readable, Writable)events(EventEmitter)path(all methods)string_decoderassert
Not Supported (need alternatives):
fs→ Use R2/KVchild_process→ Not possiblecluster→ Not applicabledgram→ Not supportednet→ Use fetch/WebSockettls→ Handled by platform
When to Load References
| Reference | Load When |
|---|---|
references/lambda-migration.md | Migrating AWS Lambda functions |
references/vercel-migration.md | Migrating from Vercel/Next.js |
references/express-migration.md | Migrating Express/Node.js apps |
references/node-compatibility.md | Node.js API compatibility issues |
Migration Checklist
1. Analyze Dependencies: Check for unsupported Node.js APIs 2. Convert to ESM: Replace require() with import 3. Update Env Access: Use env parameter instead of process.env 4. Replace File System: Use R2/KV for storage 5. Handle Async: Use ctx.waitUntil() for background tasks 6. Test Locally: Verify with wrangler dev 7. Performance Test: Ensure CPU limits aren't exceeded
See Also
workers-runtime-apis- Available APIs in Workersworkers-performance- Optimization techniquescloudflare-worker-base- Basic Workers setup
Express/Node.js to Cloudflare Workers Migration
Comprehensive guide for migrating Express and Node.js applications to Cloudflare Workers.
Express vs Workers Concepts
| Express Concept | Workers Equivalent | Notes |
|---|---|---|
app | Hono app | Similar API |
req | Request | Web standard |
res | Response | Web standard |
next() | await next() | Async middleware |
app.use() | app.use() | Nearly identical |
express.json() | Built-in | Use request.json() |
express.static() | Static Assets | Workers static serving |
req.params | c.req.param() | Hono pattern |
req.query | c.req.query() | Hono pattern |
req.body | await c.req.json() | Async in Workers |
Basic Migration
Hello World
// Express
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(3000);
// Hono (Workers)
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => c.text('Hello World'));
export default app;JSON Response
// Express
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// Hono
app.get('/api/users', (c) => c.json({ users: [] }));Request Body
// Express
app.use(express.json());
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
res.status(201).json({ name, email });
});
// Hono (no middleware needed)
app.post('/api/users', async (c) => {
const { name, email } = await c.req.json();
return c.json({ name, email }, 201);
});Middleware Migration
Basic Middleware
// Express
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Hono
app.use('*', async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`);
await next();
});Authentication Middleware
// Express
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
app.use('/api', authMiddleware);
// Hono
import { jwt } from 'hono/jwt';
// Option 1: Built-in JWT middleware
app.use('/api/*', jwt({ secret: env.JWT_SECRET }));
app.get('/api/protected', (c) => {
const payload = c.get('jwtPayload');
return c.json({ user: payload });
});
// Option 2: Custom middleware
const authMiddleware = async (c, next) => {
const token = c.req.header('Authorization')?.split(' ')[1];
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
try {
const payload = await verifyToken(token, c.env.JWT_SECRET);
c.set('user', payload);
await next();
} catch {
return c.json({ error: 'Invalid token' }, 401);
}
};
app.use('/api/*', authMiddleware);Error Handling Middleware
// Express
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
// Hono
app.onError((err, c) => {
console.error(err);
return c.json({ error: 'Internal server error' }, 500);
});
// Or with detailed error handling
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse();
}
console.error(err);
return c.json(
{
error: 'Internal server error',
message: c.env.ENVIRONMENT === 'development' ? err.message : undefined,
},
500
);
});CORS Middleware
// Express
import cors from 'cors';
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
}));
// Hono
import { cors } from 'hono/cors';
app.use('*', cors({
origin: 'https://example.com',
allowMethods: ['GET', 'POST'],
}));Route Parameters
// Express
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ id });
});
app.get('/posts/:postId/comments/:commentId', (req, res) => {
const { postId, commentId } = req.params;
res.json({ postId, commentId });
});
// Hono
app.get('/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ id });
});
app.get('/posts/:postId/comments/:commentId', (c) => {
const postId = c.req.param('postId');
const commentId = c.req.param('commentId');
return c.json({ postId, commentId });
});Query Parameters
// Express
app.get('/search', (req, res) => {
const { q, page = 1, limit = 10 } = req.query;
res.json({ q, page, limit });
});
// Hono
app.get('/search', (c) => {
const q = c.req.query('q');
const page = c.req.query('page') || '1';
const limit = c.req.query('limit') || '10';
return c.json({ q, page: parseInt(page), limit: parseInt(limit) });
});
// Or get all at once
app.get('/search', (c) => {
const { q, page = '1', limit = '10' } = c.req.query();
return c.json({ q, page: parseInt(page), limit: parseInt(limit) });
});Request Validation
// Express with express-validator
import { body, validationResult } from 'express-validator';
app.post('/users',
body('email').isEmail(),
body('name').isLength({ min: 2 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Create user...
}
);
// Hono with Zod
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
app.post('/users', zValidator('json', createUserSchema), (c) => {
const { email, name } = c.req.valid('json');
// Create user...
});Database Integration
MongoDB → D1/KV
// Express with MongoDB
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI);
app.get('/users', async (req, res) => {
const db = client.db('myapp');
const users = await db.collection('users').find().toArray();
res.json(users);
});
// Hono with D1
interface Env {
DB: D1Database;
}
const app = new Hono<{ Bindings: Env }>();
app.get('/users', async (c) => {
const { results } = await c.env.DB.prepare('SELECT * FROM users').all();
return c.json(results);
});Prisma → Drizzle
// Express with Prisma
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
app.get('/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.params.id },
include: { posts: true },
});
res.json(user);
});
// Hono with Drizzle + D1
import { drizzle } from 'drizzle-orm/d1';
import { users, posts } from './schema';
app.get('/users/:id', async (c) => {
const db = drizzle(c.env.DB);
const id = c.req.param('id');
const user = await db
.select()
.from(users)
.where(eq(users.id, id))
.leftJoin(posts, eq(posts.userId, users.id));
return c.json(user);
});Session Management
// Express with express-session
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}));
app.post('/login', (req, res) => {
req.session.userId = user.id;
res.json({ success: true });
});
// Hono with KV sessions
import { getCookie, setCookie } from 'hono/cookie';
const SESSION_TTL = 60 * 60 * 24; // 24 hours
interface Env {
SESSIONS: KVNamespace;
}
const sessionMiddleware = async (c, next) => {
const sessionId = getCookie(c, 'session_id');
if (sessionId) {
const sessionData = await c.env.SESSIONS.get(sessionId, 'json');
if (sessionData) {
c.set('session', sessionData);
}
}
await next();
// Save session after handler
const session = c.get('session');
if (session) {
const id = sessionId || crypto.randomUUID();
await c.env.SESSIONS.put(id, JSON.stringify(session), {
expirationTtl: SESSION_TTL,
});
setCookie(c, 'session_id', id, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: SESSION_TTL,
});
}
};
app.use('*', sessionMiddleware);
app.post('/login', async (c) => {
const { email, password } = await c.req.json();
const user = await authenticate(email, password);
c.set('session', { userId: user.id });
return c.json({ success: true });
});File Uploads
// Express with multer
import multer from 'multer';
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload', upload.single('file'), async (req, res) => {
const file = req.file;
// Save to S3...
res.json({ url: uploadedUrl });
});
// Hono with R2
interface Env {
BUCKET: R2Bucket;
}
app.post('/upload', async (c) => {
const formData = await c.req.formData();
const file = formData.get('file') as File;
if (!file) {
return c.json({ error: 'No file provided' }, 400);
}
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
const content = await file.arrayBuffer();
await c.env.BUCKET.put(key, content, {
httpMetadata: { contentType: file.type },
});
return c.json({ url: `https://cdn.example.com/${key}` });
});WebSocket Migration
// Express with ws
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
ws.send(`Echo: ${data}`);
});
});
// Workers with Durable Objects
export class WebSocketRoom implements DurableObject {
private sessions: Set<WebSocket> = new Set();
async fetch(request: Request): Promise<Response> {
if (request.headers.get('Upgrade') !== 'websocket') {
return new Response('Expected websocket', { status: 400 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
this.sessions.add(server);
server.addEventListener('message', (event) => {
server.send(`Echo: ${event.data}`);
});
server.addEventListener('close', () => {
this.sessions.delete(server);
});
return new Response(null, {
status: 101,
webSocket: client,
});
}
}Static Files
// Express
app.use(express.static('public'));
// Workers with Static Assets
// wrangler.jsonc
{
"assets": {
"directory": "./public"
}
}
// Or manual serving
app.get('/static/*', async (c) => {
const path = c.req.path.replace('/static/', '');
const object = await c.env.ASSETS.get(path);
if (!object) {
return c.notFound();
}
return new Response(object.body, {
headers: { 'Content-Type': object.httpMetadata?.contentType || 'text/plain' },
});
});Express Adapter (Gradual Migration)
For complex apps, use an adapter pattern:
// express-adapter.ts
type ExpressHandler = (
req: ExpressRequest,
res: ExpressResponse,
next?: () => void
) => void | Promise<void>;
interface ExpressRequest {
method: string;
url: string;
path: string;
params: Record<string, string>;
query: Record<string, string>;
headers: Record<string, string>;
body: any;
get(name: string): string | undefined;
}
class ExpressResponse {
private statusCode = 200;
private headers = new Headers();
private body: any = null;
private sent = false;
status(code: number): this {
this.statusCode = code;
return this;
}
set(name: string, value: string): this {
this.headers.set(name, value);
return this;
}
json(data: any): this {
this.headers.set('Content-Type', 'application/json');
this.body = JSON.stringify(data);
this.sent = true;
return this;
}
send(data: string): this {
this.body = data;
this.sent = true;
return this;
}
end(): this {
this.sent = true;
return this;
}
toResponse(): Response {
return new Response(this.body, {
status: this.statusCode,
headers: this.headers,
});
}
}
export function adaptExpressHandler(handler: ExpressHandler) {
return async (c: Context) => {
const url = new URL(c.req.url);
const req: ExpressRequest = {
method: c.req.method,
url: c.req.url,
path: url.pathname,
params: c.req.param() as Record<string, string>,
query: Object.fromEntries(url.searchParams),
headers: Object.fromEntries(c.req.raw.headers),
body: c.req.method !== 'GET' ? await c.req.json().catch(() => ({})) : {},
get(name: string) {
return c.req.header(name);
},
};
const res = new ExpressResponse();
await handler(req, res);
return res.toResponse();
};
}
// Usage
import { existingHandler } from './existing-express-code';
app.get('/api/legacy', adaptExpressHandler(existingHandler));Migration Checklist
1. [ ] Replace Express with Hono 2. [ ] Convert require() to import 3. [ ] Replace process.env with env parameter 4. [ ] Convert sync middleware to async 5. [ ] Replace req.body with await c.req.json() 6. [ ] Replace res.json() with c.json() 7. [ ] Migrate database to D1/KV 8. [ ] Migrate file storage to R2 9. [ ] Replace sessions with KV-based sessions 10. [ ] Update WebSocket to Durable Objects
AWS Lambda to Cloudflare Workers Migration
Comprehensive guide for migrating AWS Lambda functions to Cloudflare Workers.
Key Differences
| Aspect | AWS Lambda | Cloudflare Workers |
|---|---|---|
| Handler Signature | (event, context) | fetch(request, env, ctx) |
| Response Format | { statusCode, body } | Response object |
| Triggers | API Gateway, S3, etc. | HTTP, Cron, Queues |
| Cold Starts | 100-500ms | ~0ms |
| Execution Time | 15 minutes | 50ms CPU |
| Memory | Up to 10GB | 128MB |
| Environment | process.env | env parameter |
Handler Migration
Basic Lambda Handler
// AWS Lambda
import { APIGatewayProxyHandler } from 'aws-lambda';
export const handler: APIGatewayProxyHandler = async (event, context) => {
const body = JSON.parse(event.body || '{}');
const userId = event.pathParameters?.id;
const query = event.queryStringParameters?.search;
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Success', userId, query }),
};
};
// Cloudflare Workers
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const body = await request.json();
const userId = url.pathname.split('/')[2]; // Extract from path
const query = url.searchParams.get('search');
return Response.json({ message: 'Success', userId, query });
},
};Lambda Adapter Pattern
Create a compatibility layer for minimal code changes:
// lambda-adapter.ts
interface LambdaEvent {
body: string | null;
headers: Record<string, string>;
httpMethod: string;
path: string;
pathParameters: Record<string, string> | null;
queryStringParameters: Record<string, string> | null;
requestContext: {
requestId: string;
};
}
interface LambdaContext {
awsRequestId: string;
getRemainingTimeInMillis: () => number;
}
interface LambdaResponse {
statusCode: number;
headers?: Record<string, string>;
body: string;
isBase64Encoded?: boolean;
}
type LambdaHandler = (event: LambdaEvent, context: LambdaContext) => Promise<LambdaResponse>;
export function adaptLambdaHandler(handler: LambdaHandler) {
return {
async fetch(request: Request, env: Env): Promise<Response> {
// Convert Request to Lambda Event
const url = new URL(request.url);
const event: LambdaEvent = {
body: request.method !== 'GET' ? await request.text() : null,
headers: Object.fromEntries(request.headers),
httpMethod: request.method,
path: url.pathname,
pathParameters: extractPathParams(url.pathname),
queryStringParameters: Object.fromEntries(url.searchParams),
requestContext: {
requestId: crypto.randomUUID(),
},
};
const context: LambdaContext = {
awsRequestId: crypto.randomUUID(),
getRemainingTimeInMillis: () => 50, // Workers limit
};
// Call Lambda handler
const result = await handler(event, context);
// Convert Lambda Response to Response
const headers = new Headers(result.headers || {});
if (result.isBase64Encoded) {
const body = Uint8Array.from(atob(result.body), (c) => c.charCodeAt(0));
return new Response(body, {
status: result.statusCode,
headers,
});
}
return new Response(result.body, {
status: result.statusCode,
headers,
});
},
};
}
function extractPathParams(path: string): Record<string, string> | null {
// Implement based on your routing pattern
const match = path.match(/\/api\/users\/(\w+)/);
if (match) {
return { id: match[1] };
}
return null;
}Using the Adapter
// Existing Lambda handler (minimal changes)
const handler = async (event, context) => {
const userId = event.pathParameters?.id;
return {
statusCode: 200,
body: JSON.stringify({ userId }),
};
};
// Export for Workers
import { adaptLambdaHandler } from './lambda-adapter';
export default adaptLambdaHandler(handler);Common Patterns
API Gateway Authorizer → Workers Middleware
// Lambda Authorizer
export const authorizer = async (event) => {
const token = event.headers.Authorization;
// Validate token...
return {
principalId: 'user123',
policyDocument: {
Statement: [{ Effect: 'Allow', Action: 'execute-api:Invoke', Resource: '*' }],
},
};
};
// Workers Middleware
async function authMiddleware(request: Request, env: Env): Promise<Response | null> {
const token = request.headers.get('Authorization');
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const payload = await verifyToken(token, env);
// Attach to request context
request.headers.set('X-User-Id', payload.userId);
return null; // Continue to handler
} catch {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Run middleware
const authResponse = await authMiddleware(request, env);
if (authResponse) return authResponse;
// Continue with handler
return handleRequest(request, env);
},
};S3 Trigger → R2 Event Notification
// Lambda S3 Trigger
export const handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
await processFile(bucket, key);
}
};
// Workers Queue Consumer (R2 events via Queues)
export default {
async queue(batch: MessageBatch<R2EventMessage>, env: Env): Promise<void> {
for (const message of batch.messages) {
const { bucket, key, action } = message.body;
if (action === 'PutObject') {
await processFile(env.R2_BUCKET, key);
}
message.ack();
}
},
};
interface R2EventMessage {
bucket: string;
key: string;
action: 'PutObject' | 'DeleteObject';
}DynamoDB → D1
// Lambda with DynamoDB
import { DynamoDB } from 'aws-sdk';
const dynamo = new DynamoDB.DocumentClient();
export const handler = async (event) => {
const result = await dynamo
.get({
TableName: 'users',
Key: { id: event.pathParameters.id },
})
.promise();
return { statusCode: 200, body: JSON.stringify(result.Item) };
};
// Workers with D1
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const id = url.pathname.split('/').pop();
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first();
if (!user) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(user);
},
};SQS → Cloudflare Queues
// Lambda SQS Consumer
export const handler = async (event) => {
for (const record of event.Records) {
const message = JSON.parse(record.body);
await processMessage(message);
}
};
// Workers Queue Consumer
export default {
async queue(batch: MessageBatch<QueueMessage>, env: Env): Promise<void> {
for (const message of batch.messages) {
try {
await processMessage(message.body);
message.ack();
} catch (error) {
message.retry();
}
}
},
};AWS SDK Migration
Secrets Manager → Workers Secrets
// Lambda with Secrets Manager
import { SecretsManager } from 'aws-sdk';
const sm = new SecretsManager();
export const handler = async () => {
const secret = await sm
.getSecretValue({ SecretId: 'my-api-key' })
.promise();
const apiKey = secret.SecretString;
// Use apiKey...
};
// Workers with Secrets
interface Env {
API_KEY: string; // Configured via wrangler secret
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const apiKey = env.API_KEY;
// Use apiKey...
},
};S3 → R2
// Lambda with S3
import { S3 } from 'aws-sdk';
const s3 = new S3();
export const handler = async (event) => {
const { key } = event.pathParameters;
const object = await s3
.getObject({
Bucket: 'my-bucket',
Key: key,
})
.promise();
return {
statusCode: 200,
headers: { 'Content-Type': object.ContentType },
body: object.Body.toString('base64'),
isBase64Encoded: true,
};
};
// Workers with R2
interface Env {
BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.BUCKET.get(key);
if (!object) {
return new Response('Not found', { status: 404 });
}
const headers = new Headers();
headers.set('Content-Type', object.httpMetadata?.contentType || 'application/octet-stream');
return new Response(object.body, { headers });
},
};Execution Model Differences
Background Processing
// Lambda (15 min execution)
export const handler = async (event) => {
// Long running task
for (const item of event.items) {
await processItem(item); // Can take minutes
}
return { statusCode: 200 };
};
// Workers (background with waitUntil)
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const body = await request.json();
// Return immediately
const response = Response.json({ status: 'processing' }, { status: 202 });
// Continue processing after response
ctx.waitUntil(
(async () => {
for (const item of body.items) {
await processItem(item);
}
})()
);
return response;
},
};Long-Running Tasks → Durable Objects
// Lambda Step Functions → Durable Objects Workflow
export class ProcessingWorkflow implements DurableObject {
private state: DurableObjectState;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const { items } = await request.json();
// Store items for processing
await this.state.storage.put('items', items);
await this.state.storage.put('processed', 0);
// Start processing
await this.processNext();
return Response.json({ status: 'started' });
}
async processNext(): Promise<void> {
const items = await this.state.storage.get<any[]>('items');
const processed = await this.state.storage.get<number>('processed') || 0;
if (processed >= items!.length) {
return; // Done
}
// Process one item
await processItem(items![processed]);
// Update progress
await this.state.storage.put('processed', processed + 1);
// Schedule next (use alarm for reliability)
await this.state.storage.setAlarm(Date.now() + 100);
}
async alarm(): Promise<void> {
await this.processNext();
}
}Migration Checklist
1. [ ] Replace event/context with request/env/ctx 2. [ ] Convert response format to Response object 3. [ ] Replace process.env with env parameter 4. [ ] Replace AWS SDK calls with Workers equivalents 5. [ ] Replace DynamoDB with D1 or KV 6. [ ] Replace S3 with R2 7. [ ] Replace SQS with Queues 8. [ ] Convert long-running tasks to waitUntil or DO 9. [ ] Update tests for Workers environment 10. [ ] Configure wrangler.jsonc with bindings
Node.js API Compatibility in Workers
Comprehensive reference for Node.js API compatibility and polyfills in Cloudflare Workers.
Compatibility Flags
Enable Node.js compatibility in wrangler.jsonc:
{
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat_v2"]
}Note: nodejs_compat_v2 supersedes nodejs_compat with better support.
Supported APIs
Buffer
// Import explicitly
import { Buffer } from 'node:buffer';
// Usage
const buf = Buffer.from('Hello');
const base64 = buf.toString('base64');
const hex = buf.toString('hex');
const utf8 = buf.toString('utf-8');
// ArrayBuffer interop
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const newBuf = Buffer.from(arrayBuffer);Crypto
import { createHash, createHmac, randomBytes, randomUUID } from 'node:crypto';
// Hashing
const hash = createHash('sha256').update('data').digest('hex');
// HMAC
const hmac = createHmac('sha256', 'secret').update('data').digest('base64');
// Random bytes
const bytes = randomBytes(32);
// UUID
const uuid = randomUUID();
// Note: Some methods not supported (scrypt, etc.)
// Use Web Crypto API instead:
const webHash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('data'));Stream
import { Readable, Writable, Transform, pipeline } from 'node:stream';
import { promisify } from 'node:util';
const pipelineAsync = promisify(pipeline);
// Create readable stream
const readable = Readable.from(['Hello', ' ', 'World']);
// Transform stream
const uppercase = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
// Use with fetch Response
const response = new Response(Readable.toWeb(readable));
// Web Stream to Node Stream
const webStream = response.body;
const nodeStream = Readable.fromWeb(webStream);Events
import { EventEmitter } from 'node:events';
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on('event', (data) => {
console.log('Received:', data);
});
emitter.emit('event', { message: 'Hello' });Path
import path from 'node:path';
// All methods supported
const joined = path.join('dir', 'subdir', 'file.txt');
const parsed = path.parse('/home/user/file.txt');
const basename = path.basename('/home/user/file.txt');
const dirname = path.dirname('/home/user/file.txt');
const extname = path.extname('/home/user/file.txt');
const normalized = path.normalize('/home//user/../user/file.txt');Util
import { promisify, inspect, types } from 'node:util';
// Promisify
const wait = promisify(setTimeout);
await wait(1000);
// Inspect
const str = inspect({ nested: { object: true } });
// Type checking
types.isDate(new Date()); // true
types.isPromise(Promise.resolve()); // true
types.isArrayBuffer(new ArrayBuffer(8)); // trueAssert
import assert from 'node:assert';
assert.ok(true);
assert.strictEqual(1 + 1, 2);
assert.deepStrictEqual({ a: 1 }, { a: 1 });
assert.throws(() => { throw new Error('test'); });
await assert.rejects(Promise.reject(new Error('test')));String Decoder
import { StringDecoder } from 'node:string_decoder';
const decoder = new StringDecoder('utf8');
const result = decoder.write(Buffer.from('Hello'));URL
import { URL, URLSearchParams } from 'node:url';
// Standard Web APIs (always available)
const url = new URL('https://example.com/path?query=value');
const params = new URLSearchParams({ a: '1', b: '2' });Querystring
import querystring from 'node:querystring';
const parsed = querystring.parse('foo=bar&baz=qux');
const stringified = querystring.stringify({ foo: 'bar', baz: 'qux' });Unsupported APIs
File System (fs)
// NOT SUPPORTED
import fs from 'node:fs'; // ❌
// ALTERNATIVES
// Use R2 for file storage
const file = await env.BUCKET.get('path/to/file');
const content = await file?.text();
// Use KV for small files
const data = await env.KV.get('file-content');
// Read static assets (bundled at build)
import content from './data.txt';Child Process
// NOT SUPPORTED
import { spawn, exec } from 'node:child_process'; // ❌
// No direct alternative - Workers can't spawn processes
// Consider:
// 1. Move computation to build time
// 2. Use external API for processing
// 3. Use WebAssembly for CPU-intensive tasksNet/TLS
// NOT SUPPORTED
import net from 'node:net'; // ❌
import tls from 'node:tls'; // ❌
// ALTERNATIVES
// Use fetch for HTTP
const response = await fetch('https://api.example.com');
// Use WebSocket for persistent connections
const ws = new WebSocket('wss://api.example.com');
// TCP connections via Cloudflare Hyperdrive
const socket = env.HYPERDRIVE.connect();Cluster/Worker Threads
// NOT SUPPORTED
import cluster from 'node:cluster'; // ❌
import { Worker } from 'node:worker_threads'; // ❌
// Workers automatically scale globally
// Use Durable Objects for coordinationOS/Process
// PARTIALLY SUPPORTED
// process.env - use env parameter instead
const apiKey = env.API_KEY;
// process.version, process.platform - limited support
// os module - not supported
// ALTERNATIVE
const runtime = {
platform: 'cloudflare-workers',
version: '1.0.0',
};Polyfill Patterns
fs.readFile → KV/R2
// Original Node.js
import fs from 'node:fs/promises';
const content = await fs.readFile('config.json', 'utf-8');
const config = JSON.parse(content);
// Workers with KV
interface Env {
CONFIG: KVNamespace;
}
async function getConfig(env: Env) {
const content = await env.CONFIG.get('config.json');
return content ? JSON.parse(content) : null;
}
// Workers with bundled file
import config from './config.json';setTimeout/setInterval → Scheduled Tasks
// Original Node.js
setInterval(() => {
cleanupOldData();
}, 60000);
// Workers with Cron Triggers
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
await cleanupOldData(env);
},
};
// wrangler.jsonc
{
"triggers": {
"crons": ["*/1 * * * *"] // Every minute
}
}setTimeout → waitUntil
// Original Node.js (delay response)
setTimeout(() => {
cleanup();
}, 0);
res.send('OK');
// Workers (background processing)
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Return immediately
const response = new Response('OK');
// Run cleanup in background
ctx.waitUntil(cleanup(env));
return response;
},
};http.request → fetch
// Original Node.js
import http from 'node:http';
const req = http.request({
hostname: 'api.example.com',
port: 443,
path: '/data',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { console.log(JSON.parse(data)); });
});
req.write(JSON.stringify({ key: 'value' }));
req.end();
// Workers with fetch
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
});
const data = await response.json();DNS → fetch with DNS over HTTPS
// Original Node.js
import dns from 'node:dns/promises';
const addresses = await dns.resolve('example.com', 'A');
// Workers (limited support)
// Use fetch - DNS is handled by platform
// For explicit DNS lookups, use DNS over HTTPS
async function resolveDNS(domain: string): Promise<string[]> {
const response = await fetch(
`https://cloudflare-dns.com/dns-query?name=${domain}&type=A`,
{ headers: { Accept: 'application/dns-json' } }
);
const data = await response.json();
return data.Answer?.map((a: any) => a.data) || [];
}Common Migration Patterns
Environment Variables
// Node.js
const config = {
apiKey: process.env.API_KEY,
dbUrl: process.env.DATABASE_URL,
debug: process.env.DEBUG === 'true',
};
// Workers
interface Env {
API_KEY: string;
DATABASE_URL: string;
DEBUG: string;
}
function getConfig(env: Env) {
return {
apiKey: env.API_KEY,
dbUrl: env.DATABASE_URL,
debug: env.DEBUG === 'true',
};
}Global Variables
// Node.js
global.cache = new Map();
// Workers (use KV or Durable Objects for persistence)
// In-memory cache (per-isolate, not shared)
const cache = new Map();
// Or with caches API
const cache = await caches.open('my-cache');
await cache.put(request, response);
const cached = await cache.match(request);
// Or with KV
await env.CACHE.put('key', 'value');
const value = await env.CACHE.get('key');Async Local Storage
// Node.js
import { AsyncLocalStorage } from 'node:async_hooks';
const requestContext = new AsyncLocalStorage<{ requestId: string }>();
// Works in Workers with nodejs_compat_v2
export default {
async fetch(request: Request): Promise<Response> {
const requestId = crypto.randomUUID();
return requestContext.run({ requestId }, async () => {
// Access context anywhere
const ctx = requestContext.getStore();
console.log('Request ID:', ctx?.requestId);
return handleRequest(request);
});
},
};Dependency Compatibility
Common npm packages and their Workers compatibility:
| Package | Status | Alternative |
|---|---|---|
axios | ⚠️ Partial | Use fetch |
lodash | ✅ Works | - |
moment | ✅ Works | Use date-fns (smaller) |
uuid | ✅ Works | Use crypto.randomUUID() |
bcrypt | ❌ Native | Use bcryptjs |
sharp | ❌ Native | Use Cloudflare Images |
puppeteer | ❌ Native | Use Browser Rendering API |
pg | ⚠️ Needs adapter | Use Hyperdrive |
mysql2 | ⚠️ Needs adapter | Use Hyperdrive |
mongoose | ❌ MongoDB | Use D1/Workers KV |
prisma | ⚠️ Needs D1 adapter | Use Drizzle |
zod | ✅ Works | - |
joi | ✅ Works | - |
jsonwebtoken | ⚠️ Partial | Use jose |
express | ❌ Not compatible | Use Hono |
Testing Compatibility
Check if a package works in Workers:
// Test in wrangler dev
export default {
async fetch(): Promise<Response> {
try {
// Import and test package
const _ = await import('lodash');
const result = _.chunk([1, 2, 3, 4], 2);
return Response.json({ works: true, result });
} catch (error) {
return Response.json({
works: false,
error: error.message,
});
}
},
};Migration Checklist
1. [ ] Enable nodejs_compat_v2 flag 2. [ ] Replace require() with ESM import 3. [ ] Add node: prefix to Node.js imports 4. [ ] Replace fs with KV/R2 5. [ ] Replace http/https with fetch 6. [ ] Replace process.env with env parameter 7. [ ] Check npm dependencies for compatibility 8. [ ] Replace native modules with pure JS alternatives 9. [ ] Test with wrangler dev 10. [ ] Profile for CPU limit compliance
Vercel/Next.js to Cloudflare Workers Migration
Comprehensive guide for migrating from Vercel and Next.js to Cloudflare Workers.
Migration Paths
| Source | Target | Complexity | Approach |
|---|---|---|---|
| API Routes | Workers + Hono | Low | Direct conversion |
| Full Next.js | Workers + OpenNext | Medium | Adapter-based |
| Edge Middleware | Workers | Low | Direct conversion |
| Edge Functions | Workers | Low | Nearly identical |
API Routes Migration
Basic API Route
// Vercel: pages/api/hello.ts
import { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { name = 'World' } = req.query;
res.status(200).json({ message: `Hello ${name}` });
}
// Workers: src/index.ts
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const name = url.searchParams.get('name') || 'World';
return Response.json({ message: `Hello ${name}` });
},
};API Route with Methods
// Vercel: pages/api/users/[id].ts
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query;
switch (req.method) {
case 'GET':
const user = await getUser(id as string);
return res.status(200).json(user);
case 'PUT':
const updated = await updateUser(id as string, req.body);
return res.status(200).json(updated);
case 'DELETE':
await deleteUser(id as string);
return res.status(204).end();
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).end();
}
}
// Workers with Hono: src/index.ts
import { Hono } from 'hono';
interface Env {
DB: D1Database;
}
const app = new Hono<{ Bindings: Env }>();
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id');
const user = await getUser(c.env.DB, id);
return c.json(user);
});
app.put('/api/users/:id', async (c) => {
const id = c.req.param('id');
const body = await c.req.json();
const updated = await updateUser(c.env.DB, id, body);
return c.json(updated);
});
app.delete('/api/users/:id', async (c) => {
const id = c.req.param('id');
await deleteUser(c.env.DB, id);
return c.body(null, 204);
});
export default app;API Route with Database
// Vercel: pages/api/posts.ts
import { sql } from '@vercel/postgres';
export default async function handler(req, res) {
if (req.method === 'GET') {
const { rows } = await sql`SELECT * FROM posts ORDER BY created_at DESC LIMIT 10`;
return res.json(rows);
}
if (req.method === 'POST') {
const { title, content } = req.body;
const { rows } = await sql`
INSERT INTO posts (title, content)
VALUES (${title}, ${content})
RETURNING *
`;
return res.status(201).json(rows[0]);
}
}
// Workers with D1: src/index.ts
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/posts') {
if (request.method === 'GET') {
const { results } = await env.DB.prepare(
'SELECT * FROM posts ORDER BY created_at DESC LIMIT 10'
).all();
return Response.json(results);
}
if (request.method === 'POST') {
const { title, content } = await request.json();
const result = await env.DB.prepare(
'INSERT INTO posts (title, content) VALUES (?, ?) RETURNING *'
)
.bind(title, content)
.first();
return Response.json(result, { status: 201 });
}
}
return new Response('Not found', { status: 404 });
},
};Edge Middleware Migration
// Vercel: middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check auth
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('X-Request-Id', crypto.randomUUID());
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
// Workers: src/middleware.ts
interface Env {
JWT_SECRET: string;
}
export async function handleMiddleware(
request: Request,
env: Env
): Promise<Response | null> {
const url = new URL(request.url);
// Check auth for dashboard routes
if (url.pathname.startsWith('/dashboard')) {
const token = getCookie(request, 'token');
if (!token) {
return Response.redirect(new URL('/login', request.url));
}
try {
await verifyToken(token, env.JWT_SECRET);
} catch {
return Response.redirect(new URL('/login', request.url));
}
}
// Continue with request, add headers to response later
return null;
}
function getCookie(request: Request, name: string): string | null {
const cookies = request.headers.get('Cookie');
if (!cookies) return null;
const match = cookies.match(new RegExp(`${name}=([^;]+)`));
return match ? match[1] : null;
}Full Next.js Migration (OpenNext)
For complete Next.js apps, use the OpenNext adapter:
# Install OpenNext
npm install @opennextjs/cloudflare
# Add to package.json
# "build": "npx opennextjs-cloudflare"// app/api/users/route.ts (App Router)
import { getCloudflareContext } from '@opennextjs/cloudflare';
export async function GET() {
const { env } = await getCloudflareContext();
const { results } = await env.DB.prepare('SELECT * FROM users').all();
return Response.json(results);
}
export async function POST(request: Request) {
const { env } = await getCloudflareContext();
const body = await request.json();
const result = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
)
.bind(body.name, body.email)
.first();
return Response.json(result, { status: 201 });
}// wrangler.jsonc
{
"name": "my-nextjs-app",
"main": ".open-next/worker.js",
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat_v2"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "xxx" }
]
}Server Actions Migration
// Vercel: app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { sql } from '@vercel/postgres';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await sql`INSERT INTO posts (title, content) VALUES (${title}, ${content})`;
revalidatePath('/posts');
}
// Workers + Next.js (OpenNext): app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { getCloudflareContext } from '@opennextjs/cloudflare';
export async function createPost(formData: FormData) {
const { env } = await getCloudflareContext();
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await env.DB.prepare('INSERT INTO posts (title, content) VALUES (?, ?)')
.bind(title, content)
.run();
revalidatePath('/posts');
}Storage Migration
Vercel Blob → R2
// Vercel: pages/api/upload.ts
import { put } from '@vercel/blob';
export default async function handler(req, res) {
const file = req.body;
const blob = await put('my-file.txt', file, { access: 'public' });
res.json({ url: blob.url });
}
// Workers: src/upload.ts
interface Env {
BUCKET: R2Bucket;
}
export async function handleUpload(request: Request, env: Env): Promise<Response> {
const file = await request.arrayBuffer();
const key = 'my-file.txt';
await env.BUCKET.put(key, file, {
httpMetadata: {
contentType: request.headers.get('Content-Type') || 'application/octet-stream',
},
});
// Return public URL (configure R2 custom domain)
return Response.json({ url: `https://cdn.example.com/${key}` });
}Vercel KV → Cloudflare KV
// Vercel: pages/api/cache.ts
import { kv } from '@vercel/kv';
export default async function handler(req, res) {
if (req.method === 'GET') {
const value = await kv.get('my-key');
return res.json({ value });
}
if (req.method === 'POST') {
await kv.set('my-key', req.body.value, { ex: 3600 });
return res.json({ success: true });
}
}
// Workers: src/cache.ts
interface Env {
KV: KVNamespace;
}
export async function handleCache(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (request.method === 'GET') {
const value = await env.KV.get('my-key');
return Response.json({ value });
}
if (request.method === 'POST') {
const body = await request.json();
await env.KV.put('my-key', body.value, { expirationTtl: 3600 });
return Response.json({ success: true });
}
return new Response('Method not allowed', { status: 405 });
}ISR/SSG Migration
// Vercel: pages/posts/[id].tsx
export async function getStaticProps({ params }) {
const post = await getPost(params.id);
return {
props: { post },
revalidate: 60, // ISR: revalidate every 60 seconds
};
}
export async function getStaticPaths() {
const posts = await getAllPosts();
return {
paths: posts.map((p) => ({ params: { id: p.id } })),
fallback: 'blocking',
};
}
// Workers + Next.js (OpenNext handles ISR automatically)
// Or manual implementation:
interface Env {
DB: D1Database;
KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const match = url.pathname.match(/^\/posts\/(\w+)$/);
if (!match) {
return new Response('Not found', { status: 404 });
}
const id = match[1];
const cacheKey = `post:${id}`;
// Check cache
const cached = await env.KV.get(cacheKey, 'json');
if (cached) {
// Return cached, but revalidate in background
const response = Response.json(cached);
// Background revalidation
// (Simplified - real implementation uses stale-while-revalidate)
return response;
}
// Fetch from database
const post = await env.DB.prepare('SELECT * FROM posts WHERE id = ?')
.bind(id)
.first();
if (!post) {
return new Response('Not found', { status: 404 });
}
// Cache for 60 seconds
await env.KV.put(cacheKey, JSON.stringify(post), { expirationTtl: 60 });
return Response.json(post);
},
};Environment Variables
// Vercel: vercel.json
{
"env": {
"DATABASE_URL": "@database_url",
"API_KEY": "@api_key"
}
}
// Vercel: usage
const dbUrl = process.env.DATABASE_URL;
// Workers: wrangler.jsonc
{
"vars": {
"DATABASE_URL": "postgres://..."
}
}
// Workers: secrets (via CLI)
// npx wrangler secret put API_KEY
// Workers: usage
interface Env {
DATABASE_URL: string;
API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const dbUrl = env.DATABASE_URL;
const apiKey = env.API_KEY;
// ...
},
};Migration Checklist
1. [ ] Identify API routes to migrate 2. [ ] Choose approach: Hono (API only) or OpenNext (full Next.js) 3. [ ] Replace @vercel/postgres with D1 4. [ ] Replace @vercel/kv with Cloudflare KV 5. [ ] Replace @vercel/blob with R2 6. [ ] Update middleware to Workers format 7. [ ] Configure wrangler.jsonc with bindings 8. [ ] Migrate environment variables and secrets 9. [ ] Update Server Actions to use Cloudflare context 10. [ ] Test ISR/SSG behavior with caching
#!/bin/bash
# Migration Analysis Script
#
# Analyzes a codebase for Workers migration compatibility.
# Detects Node.js APIs, dependencies, and potential issues.
#
# Usage:
# ./scripts/analyze-migration.sh [directory] [--format json|text]
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Defaults
TARGET_DIR="${1:-.}"
FORMAT="text"
VERBOSE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--format)
FORMAT="$2"
shift 2
;;
--verbose|-v)
VERBOSE=true
shift
;;
--help|-h)
echo "Usage: $0 [directory] [options]"
echo ""
echo "Options:"
echo " --format json|text Output format (default: text)"
echo " --verbose, -v Show detailed findings"
echo " --help, -h Show this help"
exit 0
;;
*)
if [[ -d "$1" ]]; then
TARGET_DIR="$1"
fi
shift
;;
esac
done
# Validate directory
if [[ ! -d "$TARGET_DIR" ]]; then
echo -e "${RED}Error: Directory not found: $TARGET_DIR${NC}"
exit 1
fi
cd "$TARGET_DIR"
# ==========================================
# DETECTION FUNCTIONS
# ==========================================
# Detect platform
detect_platform() {
if [[ -f "vercel.json" ]] || grep -q '"vercel"' package.json 2>/dev/null; then
echo "vercel"
elif grep -q "aws-lambda" package.json 2>/dev/null || [[ -f "serverless.yml" ]]; then
echo "lambda"
elif grep -q '"express"' package.json 2>/dev/null; then
echo "express"
elif grep -q '"next"' package.json 2>/dev/null; then
echo "nextjs"
else
echo "node"
fi
}
# Count Node.js API usage
count_node_apis() {
local pattern="$1"
local count=0
if [[ -d "src" ]] || [[ -d "pages" ]] || [[ -d "api" ]]; then
count=$(grep -r "$pattern" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" . 2>/dev/null | wc -l | tr -d ' ')
fi
echo "$count"
}
# Find files with pattern
find_files_with_pattern() {
local pattern="$1"
grep -rl "$pattern" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" . 2>/dev/null || true
}
# ==========================================
# ANALYSIS
# ==========================================
echo -e "${BLUE}=== Workers Migration Analysis ===${NC}"
echo -e "Directory: ${CYAN}$TARGET_DIR${NC}"
echo ""
# Platform detection
PLATFORM=$(detect_platform)
echo -e "${BLUE}Platform Detected:${NC} ${CYAN}$PLATFORM${NC}"
echo ""
# Initialize counters
CRITICAL=0
WARNING=0
INFO=0
COMPATIBLE=0
# Results arrays
declare -a CRITICAL_ISSUES
declare -a WARNING_ISSUES
declare -a INFO_ISSUES
declare -a COMPATIBLE_ITEMS
# ==========================================
# CHECK NODE.JS APIS
# ==========================================
echo -e "${BLUE}Checking Node.js API usage...${NC}"
echo ""
# fs module
FS_COUNT=$(count_node_apis "require('fs')\|from 'fs'\|from 'node:fs'")
if [[ $FS_COUNT -gt 0 ]]; then
CRITICAL_ISSUES+=("fs module: $FS_COUNT usages (use KV/R2 instead)")
((CRITICAL++))
fi
# child_process
CP_COUNT=$(count_node_apis "require('child_process')\|from 'child_process'")
if [[ $CP_COUNT -gt 0 ]]; then
CRITICAL_ISSUES+=("child_process: $CP_COUNT usages (not supported)")
((CRITICAL++))
fi
# net/tls
NET_COUNT=$(count_node_apis "require('net')\|from 'net'\|require('tls')\|from 'tls'")
if [[ $NET_COUNT -gt 0 ]]; then
CRITICAL_ISSUES+=("net/tls modules: $NET_COUNT usages (use fetch/WebSocket)")
((CRITICAL++))
fi
# cluster/worker_threads
CLUSTER_COUNT=$(count_node_apis "require('cluster')\|from 'cluster'\|require('worker_threads')")
if [[ $CLUSTER_COUNT -gt 0 ]]; then
CRITICAL_ISSUES+=("cluster/worker_threads: $CLUSTER_COUNT usages (not supported)")
((CRITICAL++))
fi
# dgram
DGRAM_COUNT=$(count_node_apis "require('dgram')\|from 'dgram'")
if [[ $DGRAM_COUNT -gt 0 ]]; then
CRITICAL_ISSUES+=("dgram (UDP): $DGRAM_COUNT usages (not supported)")
((CRITICAL++))
fi
# process.env direct usage
ENV_COUNT=$(count_node_apis "process\.env\.")
if [[ $ENV_COUNT -gt 0 ]]; then
WARNING_ISSUES+=("process.env: $ENV_COUNT usages (use env parameter)")
((WARNING++))
fi
# require() usage
REQUIRE_COUNT=$(count_node_apis "require(")
if [[ $REQUIRE_COUNT -gt 0 ]]; then
WARNING_ISSUES+=("require(): $REQUIRE_COUNT usages (convert to ESM)")
((WARNING++))
fi
# global usage
GLOBAL_COUNT=$(count_node_apis "global\.\|globalThis\.")
if [[ $GLOBAL_COUNT -gt 0 ]]; then
WARNING_ISSUES+=("global/globalThis: $GLOBAL_COUNT usages (limited support)")
((WARNING++))
fi
# crypto (partial support)
CRYPTO_COUNT=$(count_node_apis "require('crypto')\|from 'crypto'")
if [[ $CRYPTO_COUNT -gt 0 ]]; then
INFO_ISSUES+=("crypto module: $CRYPTO_COUNT usages (partial support with nodejs_compat)")
((INFO++))
fi
# Buffer
BUFFER_COUNT=$(count_node_apis "Buffer\.")
if [[ $BUFFER_COUNT -gt 0 ]]; then
INFO_ISSUES+=("Buffer: $BUFFER_COUNT usages (supported with nodejs_compat)")
((INFO++))
fi
# stream
STREAM_COUNT=$(count_node_apis "require('stream')\|from 'stream'")
if [[ $STREAM_COUNT -gt 0 ]]; then
INFO_ISSUES+=("stream module: $STREAM_COUNT usages (supported with nodejs_compat)")
((INFO++))
fi
# path
PATH_COUNT=$(count_node_apis "require('path')\|from 'path'")
if [[ $PATH_COUNT -gt 0 ]]; then
COMPATIBLE_ITEMS+=("path module: $PATH_COUNT usages (fully supported)")
((COMPATIBLE++))
fi
# util
UTIL_COUNT=$(count_node_apis "require('util')\|from 'util'")
if [[ $UTIL_COUNT -gt 0 ]]; then
COMPATIBLE_ITEMS+=("util module: $UTIL_COUNT usages (mostly supported)")
((COMPATIBLE++))
fi
# ==========================================
# CHECK DEPENDENCIES
# ==========================================
echo -e "${BLUE}Checking dependencies...${NC}"
echo ""
if [[ -f "package.json" ]]; then
# Native modules (require compilation)
NATIVE_DEPS=("bcrypt" "sharp" "sqlite3" "canvas" "node-gyp" "better-sqlite3")
for dep in "${NATIVE_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
CRITICAL_ISSUES+=("Native dependency: $dep (needs pure JS alternative)")
((CRITICAL++))
fi
done
# Problematic deps
PROBLEM_DEPS=("puppeteer" "playwright" "selenium")
for dep in "${PROBLEM_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
CRITICAL_ISSUES+=("Browser automation: $dep (use Browser Rendering API)")
((CRITICAL++))
fi
done
# DB clients (need adapters)
DB_DEPS=("pg" "mysql" "mysql2" "mongoose" "mongodb" "redis")
for dep in "${DB_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
WARNING_ISSUES+=("Database client: $dep (use Hyperdrive or D1)")
((WARNING++))
fi
done
# ORM (may need migration)
ORM_DEPS=("prisma" "typeorm" "sequelize" "knex")
for dep in "${ORM_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
WARNING_ISSUES+=("ORM: $dep (consider Drizzle with D1)")
((WARNING++))
fi
done
# HTTP clients
HTTP_DEPS=("axios" "got" "node-fetch" "request")
for dep in "${HTTP_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
INFO_ISSUES+=("HTTP client: $dep (can use native fetch)")
((INFO++))
fi
done
# Compatible deps
COMPAT_DEPS=("lodash" "date-fns" "zod" "uuid" "jose")
for dep in "${COMPAT_DEPS[@]}"; do
if grep -q "\"$dep\"" package.json 2>/dev/null; then
COMPATIBLE_ITEMS+=("Compatible dependency: $dep")
((COMPATIBLE++))
fi
done
fi
# ==========================================
# PLATFORM-SPECIFIC CHECKS
# ==========================================
echo -e "${BLUE}Platform-specific analysis...${NC}"
echo ""
case $PLATFORM in
lambda)
# Check Lambda handlers
HANDLER_COUNT=$(count_node_apis "exports\.handler\|export const handler\|export async function handler")
INFO_ISSUES+=("Lambda handlers found: $HANDLER_COUNT (use adapter pattern)")
((INFO++))
# Check AWS SDK usage
AWS_SDK=$(count_node_apis "aws-sdk\|@aws-sdk")
if [[ $AWS_SDK -gt 0 ]]; then
WARNING_ISSUES+=("AWS SDK: $AWS_SDK usages (replace with Workers equivalents)")
((WARNING++))
fi
;;
vercel)
# Check Vercel-specific imports
VERCEL_IMPORTS=$(count_node_apis "@vercel/")
if [[ $VERCEL_IMPORTS -gt 0 ]]; then
WARNING_ISSUES+=("Vercel imports: $VERCEL_IMPORTS (replace with Cloudflare bindings)")
((WARNING++))
fi
# Check Next.js API routes
if [[ -d "pages/api" ]] || [[ -d "app/api" ]]; then
API_ROUTES=$(find pages/api app/api -name "*.ts" -o -name "*.js" 2>/dev/null | wc -l | tr -d ' ')
INFO_ISSUES+=("API routes found: $API_ROUTES (can migrate with adapter)")
((INFO++))
fi
;;
express)
# Check Express app
EXPRESS_APP=$(count_node_apis "express()")
INFO_ISSUES+=("Express apps: $EXPRESS_APP (consider Hono for similar API)")
((INFO++))
# Check middleware usage
MIDDLEWARE=$(count_node_apis "app\.use(")
if [[ $MIDDLEWARE -gt 0 ]]; then
INFO_ISSUES+=("Express middleware: $MIDDLEWARE (needs conversion)")
((INFO++))
fi
;;
nextjs)
# Check for App Router vs Pages Router
if [[ -d "app" ]]; then
INFO_ISSUES+=("Next.js App Router detected (use OpenNext adapter)")
((INFO++))
fi
if [[ -d "pages" ]]; then
INFO_ISSUES+=("Next.js Pages Router detected (use OpenNext adapter)")
((INFO++))
fi
# Check Server Actions
SERVER_ACTIONS=$(count_node_apis "'use server'")
if [[ $SERVER_ACTIONS -gt 0 ]]; then
INFO_ISSUES+=("Server Actions: $SERVER_ACTIONS files (supported with OpenNext)")
((INFO++))
fi
;;
esac
# ==========================================
# OUTPUT RESULTS
# ==========================================
if [[ "$FORMAT" == "json" ]]; then
# JSON output
echo "{"
echo " \"platform\": \"$PLATFORM\","
echo " \"summary\": {"
echo " \"critical\": $CRITICAL,"
echo " \"warning\": $WARNING,"
echo " \"info\": $INFO,"
echo " \"compatible\": $COMPATIBLE"
echo " },"
echo " \"issues\": {"
echo " \"critical\": ["
for i in "${!CRITICAL_ISSUES[@]}"; do
echo -n " \"${CRITICAL_ISSUES[$i]}\""
[[ $i -lt $((${#CRITICAL_ISSUES[@]} - 1)) ]] && echo "," || echo ""
done
echo " ],"
echo " \"warning\": ["
for i in "${!WARNING_ISSUES[@]}"; do
echo -n " \"${WARNING_ISSUES[$i]}\""
[[ $i -lt $((${#WARNING_ISSUES[@]} - 1)) ]] && echo "," || echo ""
done
echo " ],"
echo " \"info\": ["
for i in "${!INFO_ISSUES[@]}"; do
echo -n " \"${INFO_ISSUES[$i]}\""
[[ $i -lt $((${#INFO_ISSUES[@]} - 1)) ]] && echo "," || echo ""
done
echo " ]"
echo " },"
echo " \"compatible\": ["
for i in "${!COMPATIBLE_ITEMS[@]}"; do
echo -n " \"${COMPATIBLE_ITEMS[$i]}\""
[[ $i -lt $((${#COMPATIBLE_ITEMS[@]} - 1)) ]] && echo "," || echo ""
done
echo " ]"
echo "}"
else
# Text output
echo ""
echo -e "${BLUE}=== Results ===${NC}"
echo ""
# Critical issues
if [[ ${#CRITICAL_ISSUES[@]} -gt 0 ]]; then
echo -e "${RED}CRITICAL (blocking):${NC}"
for issue in "${CRITICAL_ISSUES[@]}"; do
echo -e " ${RED}✗${NC} $issue"
done
echo ""
fi
# Warnings
if [[ ${#WARNING_ISSUES[@]} -gt 0 ]]; then
echo -e "${YELLOW}WARNINGS (needs changes):${NC}"
for issue in "${WARNING_ISSUES[@]}"; do
echo -e " ${YELLOW}⚠${NC} $issue"
done
echo ""
fi
# Info
if [[ ${#INFO_ISSUES[@]} -gt 0 ]]; then
echo -e "${CYAN}INFO (may need attention):${NC}"
for issue in "${INFO_ISSUES[@]}"; do
echo -e " ${CYAN}ℹ${NC} $issue"
done
echo ""
fi
# Compatible
if [[ ${#COMPATIBLE_ITEMS[@]} -gt 0 ]]; then
echo -e "${GREEN}COMPATIBLE:${NC}"
for item in "${COMPATIBLE_ITEMS[@]}"; do
echo -e " ${GREEN}✓${NC} $item"
done
echo ""
fi
# Summary
echo -e "${BLUE}=== Summary ===${NC}"
echo ""
echo -e " Critical: ${RED}$CRITICAL${NC}"
echo -e " Warnings: ${YELLOW}$WARNING${NC}"
echo -e " Info: ${CYAN}$INFO${NC}"
echo -e " OK: ${GREEN}$COMPATIBLE${NC}"
echo ""
# Migration complexity
TOTAL_ISSUES=$((CRITICAL + WARNING))
if [[ $CRITICAL -gt 5 ]]; then
echo -e "${RED}Migration Complexity: HIGH${NC}"
echo "Consider incremental migration or significant refactoring."
elif [[ $TOTAL_ISSUES -gt 10 ]]; then
echo -e "${YELLOW}Migration Complexity: MEDIUM${NC}"
echo "Some refactoring needed, but achievable."
else
echo -e "${GREEN}Migration Complexity: LOW${NC}"
echo "Should be straightforward with adapter patterns."
fi
echo ""
# Recommendations
echo -e "${BLUE}=== Recommendations ===${NC}"
echo ""
case $PLATFORM in
lambda)
echo "1. Use Lambda adapter pattern for minimal code changes"
echo "2. Replace AWS SDK calls with Cloudflare bindings"
echo "3. Migrate DynamoDB to D1"
echo "4. Migrate S3 to R2"
;;
vercel)
echo "1. Use OpenNext adapter for full Next.js support"
echo "2. Replace @vercel/* packages with Cloudflare equivalents"
echo "3. Migrate Vercel KV to Cloudflare KV"
echo "4. Migrate Vercel Postgres to D1"
;;
express)
echo "1. Consider Hono as Express-like alternative"
echo "2. Use express adapter for gradual migration"
echo "3. Convert middleware to Hono format"
echo "4. Migrate database to D1/KV"
;;
nextjs)
echo "1. Use OpenNext adapter"
echo "2. Configure wrangler.jsonc for Pages/Workers"
echo "3. Add Cloudflare bindings via getCloudflareContext"
echo "4. Enable nodejs_compat_v2 compatibility flag"
;;
*)
echo "1. Enable nodejs_compat_v2 in wrangler.jsonc"
echo "2. Convert require() to ESM imports"
echo "3. Replace process.env with env parameter"
echo "4. Replace fs with KV/R2 storage"
;;
esac
fi
/**
* Express to Cloudflare Workers Adapter
*
* Enables gradual migration of Express handlers to Workers.
* Provides Express-like req/res interface on top of Web APIs.
*
* Usage:
* 1. Keep existing Express handler logic
* 2. Import and wrap with adaptExpressHandler
* 3. Use with Hono or raw Workers
*/
import { Context, Hono } from 'hono';
// ============================================
// TYPE DEFINITIONS
// ============================================
export interface ExpressRequest {
// Properties
method: string;
url: string;
path: string;
hostname: string;
protocol: string;
secure: boolean;
ip: string;
ips: string[];
headers: Record<string, string | string[] | undefined>;
params: Record<string, string>;
query: Record<string, string | string[]>;
body: any;
cookies: Record<string, string>;
// Methods
get(name: string): string | undefined;
header(name: string): string | undefined;
is(type: string): string | false;
accepts(...types: string[]): string | false;
}
export interface ExpressResponse {
// Status
status(code: number): ExpressResponse;
sendStatus(code: number): ExpressResponse;
// Headers
set(field: string, value: string): ExpressResponse;
set(headers: Record<string, string>): ExpressResponse;
header(field: string, value: string): ExpressResponse;
type(type: string): ExpressResponse;
contentType(type: string): ExpressResponse;
append(field: string, value: string | string[]): ExpressResponse;
// Cookies
cookie(name: string, value: string, options?: CookieOptions): ExpressResponse;
clearCookie(name: string, options?: CookieOptions): ExpressResponse;
// Sending
send(body: string | object | Buffer): ExpressResponse;
json(body: any): ExpressResponse;
text(body: string): ExpressResponse;
html(body: string): ExpressResponse;
redirect(url: string): ExpressResponse;
redirect(status: number, url: string): ExpressResponse;
end(): ExpressResponse;
// State
headersSent: boolean;
statusCode: number;
}
export interface CookieOptions {
domain?: string;
expires?: Date;
httpOnly?: boolean;
maxAge?: number;
path?: string;
sameSite?: 'strict' | 'lax' | 'none';
secure?: boolean;
}
export type NextFunction = () => Promise<void> | void;
export type ExpressHandler = (
req: ExpressRequest,
res: ExpressResponse,
next?: NextFunction
) => void | Promise<void>;
export type ExpressMiddleware = (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
) => void | Promise<void>;
// ============================================
// REQUEST IMPLEMENTATION
// ============================================
class WorkersExpressRequest implements ExpressRequest {
method: string;
url: string;
path: string;
hostname: string;
protocol: string;
secure: boolean;
ip: string;
ips: string[];
headers: Record<string, string | undefined>;
params: Record<string, string>;
query: Record<string, string | string[]>;
body: any;
cookies: Record<string, string>;
constructor(request: Request, params: Record<string, string>, body: any) {
const url = new URL(request.url);
this.method = request.method;
this.url = request.url;
this.path = url.pathname;
this.hostname = url.hostname;
this.protocol = url.protocol.replace(':', '');
this.secure = url.protocol === 'https:';
this.ip = request.headers.get('cf-connecting-ip') || '0.0.0.0';
this.ips = (request.headers.get('x-forwarded-for') || '')
.split(',')
.map((ip) => ip.trim())
.filter(Boolean);
this.params = params;
this.body = body;
// Parse headers
this.headers = {};
request.headers.forEach((value, key) => {
this.headers[key.toLowerCase()] = value;
});
// Parse query string
this.query = {};
url.searchParams.forEach((value, key) => {
const existing = this.query[key];
if (existing) {
if (Array.isArray(existing)) {
existing.push(value);
} else {
this.query[key] = [existing, value];
}
} else {
this.query[key] = value;
}
});
// Parse cookies
this.cookies = {};
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
cookieHeader.split(';').forEach((cookie) => {
const [name, ...rest] = cookie.trim().split('=');
if (name) {
this.cookies[name] = rest.join('=');
}
});
}
}
get(name: string): string | undefined {
return this.headers[name.toLowerCase()] as string | undefined;
}
header(name: string): string | undefined {
return this.get(name);
}
is(type: string): string | false {
const contentType = this.get('content-type') || '';
if (contentType.includes(type)) {
return type;
}
return false;
}
accepts(...types: string[]): string | false {
const accept = this.get('accept') || '';
for (const type of types) {
if (accept.includes(type) || accept.includes('*/*')) {
return type;
}
}
return false;
}
}
// ============================================
// RESPONSE IMPLEMENTATION
// ============================================
class WorkersExpressResponse implements ExpressResponse {
private _statusCode = 200;
private _headers = new Headers();
private _body: any = null;
private _sent = false;
private _cookies: string[] = [];
get statusCode(): number {
return this._statusCode;
}
get headersSent(): boolean {
return this._sent;
}
status(code: number): ExpressResponse {
this._statusCode = code;
return this;
}
sendStatus(code: number): ExpressResponse {
this._statusCode = code;
this._body = String(code);
this._sent = true;
return this;
}
set(field: string | Record<string, string>, value?: string): ExpressResponse {
if (typeof field === 'object') {
Object.entries(field).forEach(([k, v]) => {
this._headers.set(k, v);
});
} else if (value !== undefined) {
this._headers.set(field, value);
}
return this;
}
header(field: string, value: string): ExpressResponse {
this._headers.set(field, value);
return this;
}
type(type: string): ExpressResponse {
const mimeTypes: Record<string, string> = {
html: 'text/html',
json: 'application/json',
text: 'text/plain',
xml: 'application/xml',
};
this._headers.set('Content-Type', mimeTypes[type] || type);
return this;
}
contentType(type: string): ExpressResponse {
return this.type(type);
}
append(field: string, value: string | string[]): ExpressResponse {
const values = Array.isArray(value) ? value : [value];
values.forEach((v) => {
this._headers.append(field, v);
});
return this;
}
cookie(name: string, value: string, options: CookieOptions = {}): ExpressResponse {
const parts = [`${name}=${value}`];
if (options.domain) parts.push(`Domain=${options.domain}`);
if (options.path) parts.push(`Path=${options.path}`);
if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`);
if (options.httpOnly) parts.push('HttpOnly');
if (options.secure) parts.push('Secure');
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
this._cookies.push(parts.join('; '));
return this;
}
clearCookie(name: string, options: CookieOptions = {}): ExpressResponse {
return this.cookie(name, '', { ...options, expires: new Date(0) });
}
send(body: string | object | Buffer): ExpressResponse {
if (typeof body === 'object' && !(body instanceof ArrayBuffer)) {
return this.json(body);
}
this._body = body;
this._sent = true;
return this;
}
json(body: any): ExpressResponse {
this._headers.set('Content-Type', 'application/json');
this._body = JSON.stringify(body);
this._sent = true;
return this;
}
text(body: string): ExpressResponse {
this._headers.set('Content-Type', 'text/plain');
this._body = body;
this._sent = true;
return this;
}
html(body: string): ExpressResponse {
this._headers.set('Content-Type', 'text/html');
this._body = body;
this._sent = true;
return this;
}
redirect(statusOrUrl: number | string, url?: string): ExpressResponse {
if (typeof statusOrUrl === 'number') {
this._statusCode = statusOrUrl;
this._headers.set('Location', url!);
} else {
this._statusCode = 302;
this._headers.set('Location', statusOrUrl);
}
this._sent = true;
return this;
}
end(): ExpressResponse {
this._sent = true;
return this;
}
toResponse(): Response {
// Add cookies
this._cookies.forEach((cookie) => {
this._headers.append('Set-Cookie', cookie);
});
return new Response(this._body, {
status: this._statusCode,
headers: this._headers,
});
}
}
// ============================================
// ADAPTERS
// ============================================
/**
* Adapt a single Express handler for use with Hono
*/
export function adaptExpressHandler(handler: ExpressHandler) {
return async (c: Context): Promise<Response> => {
// Parse body
let body: any = {};
const contentType = c.req.header('content-type') || '';
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
try {
if (contentType.includes('application/json')) {
body = await c.req.json();
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await c.req.formData();
formData.forEach((value, key) => {
body[key] = value;
});
} else {
body = await c.req.text();
}
} catch {
// Body parsing failed, continue with empty body
}
}
const req = new WorkersExpressRequest(
c.req.raw,
c.req.param() as Record<string, string>,
body
);
const res = new WorkersExpressResponse();
await handler(req, res);
return res.toResponse();
};
}
/**
* Adapt Express middleware for use with Hono
*/
export function adaptExpressMiddleware(middleware: ExpressMiddleware) {
return async (c: Context, next: () => Promise<void>): Promise<void | Response> => {
let body: any = {};
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
try {
body = await c.req.json();
} catch {
// Continue without body
}
}
const req = new WorkersExpressRequest(
c.req.raw,
c.req.param() as Record<string, string>,
body
);
const res = new WorkersExpressResponse();
let nextCalled = false;
const nextFn: NextFunction = async () => {
nextCalled = true;
};
await middleware(req, res, nextFn);
if (nextCalled) {
await next();
} else if (res.headersSent) {
return res.toResponse();
}
};
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
// existing-express-handler.ts
export const getUserHandler = async (req, res) => {
const userId = req.params.id;
const user = await db.users.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
};
export const authMiddleware = (req, res, next) => {
const token = req.get('authorization');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = decodeToken(token);
next();
};
// index.ts (Workers entry point)
import { Hono } from 'hono';
import { adaptExpressHandler, adaptExpressMiddleware } from './express-adapter';
import { getUserHandler, authMiddleware } from './existing-express-handler';
const app = new Hono();
// Adapt middleware
app.use('/api/*', adaptExpressMiddleware(authMiddleware));
// Adapt handlers
app.get('/api/users/:id', adaptExpressHandler(getUserHandler));
export default app;
*/
/**
* AWS Lambda to Cloudflare Workers Adapter
*
* Enables minimal code changes when migrating Lambda handlers.
* Converts Lambda event/context to Workers Request/env.
*
* Usage:
* 1. Keep existing Lambda handler code
* 2. Import and wrap with adaptLambdaHandler
* 3. Export as Workers default
*/
// ============================================
// TYPE DEFINITIONS
// ============================================
export interface LambdaEvent {
body: string | null;
headers: Record<string, string | undefined>;
httpMethod: string;
path: string;
pathParameters: Record<string, string> | null;
queryStringParameters: Record<string, string> | null;
requestContext: {
requestId: string;
stage: string;
identity: {
sourceIp: string;
userAgent: string | null;
};
};
isBase64Encoded: boolean;
}
export interface LambdaContext {
awsRequestId: string;
functionName: string;
memoryLimitInMB: string;
getRemainingTimeInMillis: () => number;
callbackWaitsForEmptyEventLoop: boolean;
}
export interface LambdaResponse {
statusCode: number;
headers?: Record<string, string>;
multiValueHeaders?: Record<string, string[]>;
body?: string;
isBase64Encoded?: boolean;
}
export type LambdaHandler<TEnv = unknown> = (
event: LambdaEvent,
context: LambdaContext,
env?: TEnv
) => Promise<LambdaResponse>;
// ============================================
// PATH PARAMETER EXTRACTION
// ============================================
interface RoutePattern {
pattern: RegExp;
paramNames: string[];
}
const routePatterns: RoutePattern[] = [];
/**
* Register a route pattern for path parameter extraction
* @example registerRoute('/users/:id')
* @example registerRoute('/posts/:postId/comments/:commentId')
*/
export function registerRoute(pattern: string): void {
const paramNames: string[] = [];
const regexPattern = pattern.replace(/:(\w+)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
});
routePatterns.push({
pattern: new RegExp(`^${regexPattern}$`),
paramNames,
});
}
function extractPathParams(path: string): Record<string, string> | null {
for (const { pattern, paramNames } of routePatterns) {
const match = path.match(pattern);
if (match) {
const params: Record<string, string> = {};
paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return params;
}
}
return null;
}
// ============================================
// REQUEST CONVERSION
// ============================================
async function requestToLambdaEvent(request: Request): Promise<LambdaEvent> {
const url = new URL(request.url);
// Parse body
let body: string | null = null;
let isBase64Encoded = false;
if (request.method !== 'GET' && request.method !== 'HEAD') {
const contentType = request.headers.get('content-type') || '';
if (
contentType.includes('application/json') ||
contentType.includes('text/') ||
contentType.includes('application/x-www-form-urlencoded')
) {
body = await request.text();
} else {
// Binary data - base64 encode
const buffer = await request.arrayBuffer();
body = btoa(String.fromCharCode(...new Uint8Array(buffer)));
isBase64Encoded = true;
}
}
// Convert headers
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => {
headers[key.toLowerCase()] = value;
});
// Parse query string
const queryStringParameters: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
queryStringParameters[key] = value;
});
// Extract path parameters
const pathParameters = extractPathParams(url.pathname);
return {
body,
headers,
httpMethod: request.method,
path: url.pathname,
pathParameters,
queryStringParameters:
Object.keys(queryStringParameters).length > 0 ? queryStringParameters : null,
requestContext: {
requestId: crypto.randomUUID(),
stage: 'prod',
identity: {
sourceIp: request.headers.get('cf-connecting-ip') || '0.0.0.0',
userAgent: request.headers.get('user-agent'),
},
},
isBase64Encoded,
};
}
function createLambdaContext(): LambdaContext {
const startTime = Date.now();
const timeout = 50; // Workers limit in ms
return {
awsRequestId: crypto.randomUUID(),
functionName: 'cloudflare-worker',
memoryLimitInMB: '128',
getRemainingTimeInMillis: () => Math.max(0, timeout - (Date.now() - startTime)),
callbackWaitsForEmptyEventLoop: true,
};
}
// ============================================
// RESPONSE CONVERSION
// ============================================
function lambdaResponseToResponse(result: LambdaResponse): Response {
const headers = new Headers();
// Add regular headers
if (result.headers) {
Object.entries(result.headers).forEach(([key, value]) => {
headers.set(key, value);
});
}
// Add multi-value headers
if (result.multiValueHeaders) {
Object.entries(result.multiValueHeaders).forEach(([key, values]) => {
values.forEach((value) => {
headers.append(key, value);
});
});
}
// Handle body
let body: BodyInit | null = null;
if (result.body) {
if (result.isBase64Encoded) {
// Decode base64 to binary
const binaryString = atob(result.body);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
body = bytes;
} else {
body = result.body;
}
}
return new Response(body, {
status: result.statusCode,
headers,
});
}
// ============================================
// MAIN ADAPTER
// ============================================
/**
* Adapt a Lambda handler to work in Cloudflare Workers
*
* @example
* // Existing Lambda handler
* const handler = async (event, context) => {
* return { statusCode: 200, body: JSON.stringify({ hello: 'world' }) };
* };
*
* // Export for Workers
* export default adaptLambdaHandler(handler);
*/
export function adaptLambdaHandler<TEnv = unknown>(
handler: LambdaHandler<TEnv>,
options?: {
routes?: string[];
}
) {
// Register routes for path parameter extraction
if (options?.routes) {
options.routes.forEach(registerRoute);
}
return {
async fetch(request: Request, env: TEnv): Promise<Response> {
try {
// Convert Request to Lambda Event
const event = await requestToLambdaEvent(request);
const context = createLambdaContext();
// Call Lambda handler
const result = await handler(event, context, env);
// Convert Lambda Response to Response
return lambdaResponseToResponse(result);
} catch (error) {
console.error('Lambda adapter error:', error);
return new Response(
JSON.stringify({
error: 'Internal Server Error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
};
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
// existing-lambda.ts (minimal changes)
import { LambdaEvent, LambdaContext } from './lambda-adapter';
export const handler = async (event: LambdaEvent, context: LambdaContext) => {
const userId = event.pathParameters?.id;
if (!userId) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'User ID required' }),
};
}
// Existing logic...
const user = await getUser(userId);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
};
};
// index.ts (Workers entry point)
import { adaptLambdaHandler } from './lambda-adapter';
import { handler } from './existing-lambda';
export default adaptLambdaHandler(handler, {
routes: [
'/users/:id',
'/posts/:postId/comments/:commentId',
],
});
*/
// ============================================
// DynamoDB COMPATIBILITY LAYER
// ============================================
export interface D1LikeDynamoDB {
get(params: { TableName: string; Key: Record<string, any> }): Promise<{ Item?: any }>;
put(params: { TableName: string; Item: Record<string, any> }): Promise<void>;
query(params: {
TableName: string;
KeyConditionExpression: string;
ExpressionAttributeValues: Record<string, any>;
}): Promise<{ Items: any[] }>;
delete(params: { TableName: string; Key: Record<string, any> }): Promise<void>;
}
/**
* Create a DynamoDB-like interface backed by D1
*
* Note: This is a simplified adapter. Production use may need
* more sophisticated mapping depending on your DynamoDB usage.
*/
export function createDynamoDBAdapter(db: D1Database): D1LikeDynamoDB {
return {
async get({ TableName, Key }) {
const keyName = Object.keys(Key)[0];
const keyValue = Key[keyName];
const result = await db
.prepare(`SELECT * FROM ${TableName} WHERE ${keyName} = ?`)
.bind(keyValue)
.first();
return { Item: result || undefined };
},
async put({ TableName, Item }) {
const columns = Object.keys(Item);
const values = Object.values(Item);
const placeholders = columns.map(() => '?').join(', ');
await db
.prepare(
`INSERT OR REPLACE INTO ${TableName} (${columns.join(', ')}) VALUES (${placeholders})`
)
.bind(...values)
.run();
},
async query({ TableName, KeyConditionExpression, ExpressionAttributeValues }) {
// Simplified: assumes format "pk = :pk"
const match = KeyConditionExpression.match(/(\w+)\s*=\s*:(\w+)/);
if (!match) {
throw new Error('Unsupported KeyConditionExpression');
}
const [, column, placeholder] = match;
const value = ExpressionAttributeValues[`:${placeholder}`];
const { results } = await db
.prepare(`SELECT * FROM ${TableName} WHERE ${column} = ?`)
.bind(value)
.all();
return { Items: results };
},
async delete({ TableName, Key }) {
const keyName = Object.keys(Key)[0];
const keyValue = Key[keyName];
await db
.prepare(`DELETE FROM ${TableName} WHERE ${keyName} = ?`)
.bind(keyValue)
.run();
},
};
}
/**
* Node.js Compatibility Layer for Cloudflare Workers
*
* Provides polyfills and adapters for common Node.js patterns
* that need manual migration in Workers.
*
* Usage:
* 1. Import needed utilities
* 2. Replace Node.js-specific code with these adapters
*/
// ============================================
// ENVIRONMENT VARIABLES
// ============================================
/**
* Process.env adapter
* Creates a proxy that reads from Workers env bindings
*/
export function createProcessEnv<TEnv extends Record<string, any>>(
env: TEnv
): NodeJS.ProcessEnv {
return new Proxy({} as NodeJS.ProcessEnv, {
get(_, key: string) {
const value = env[key];
return typeof value === 'string' ? value : undefined;
},
has(_, key: string) {
return key in env && typeof env[key] === 'string';
},
});
}
// ============================================
// FILE SYSTEM (fs) → KV/R2
// ============================================
interface FileSystemAdapter {
readFile(path: string): Promise<string>;
readFileSync(path: string): never;
writeFile(path: string, data: string): Promise<void>;
exists(path: string): Promise<boolean>;
readdir(path: string): Promise<string[]>;
unlink(path: string): Promise<void>;
mkdir(path: string): Promise<void>;
stat(path: string): Promise<{ isFile: () => boolean; isDirectory: () => boolean }>;
}
/**
* Create fs-like adapter backed by KV
*/
export function createFsAdapterKV(kv: KVNamespace): FileSystemAdapter {
return {
async readFile(path: string): Promise<string> {
const content = await kv.get(path);
if (content === null) {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
}
return content;
},
readFileSync(): never {
throw new Error('Synchronous file operations not supported in Workers');
},
async writeFile(path: string, data: string): Promise<void> {
await kv.put(path, data);
},
async exists(path: string): Promise<boolean> {
const result = await kv.get(path);
return result !== null;
},
async readdir(path: string): Promise<string[]> {
const prefix = path.endsWith('/') ? path : `${path}/`;
const list = await kv.list({ prefix });
return list.keys.map((k) => k.name.replace(prefix, '').split('/')[0]);
},
async unlink(path: string): Promise<void> {
await kv.delete(path);
},
async mkdir(): Promise<void> {
// No-op for KV
},
async stat(path: string): Promise<{ isFile: () => boolean; isDirectory: () => boolean }> {
const exists = await kv.get(path);
return {
isFile: () => exists !== null,
isDirectory: () => false,
};
},
};
}
/**
* Create fs-like adapter backed by R2
*/
export function createFsAdapterR2(bucket: R2Bucket): FileSystemAdapter {
return {
async readFile(path: string): Promise<string> {
const object = await bucket.get(path);
if (!object) {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
}
return object.text();
},
readFileSync(): never {
throw new Error('Synchronous file operations not supported in Workers');
},
async writeFile(path: string, data: string): Promise<void> {
await bucket.put(path, data);
},
async exists(path: string): Promise<boolean> {
const head = await bucket.head(path);
return head !== null;
},
async readdir(path: string): Promise<string[]> {
const prefix = path.endsWith('/') ? path : `${path}/`;
const list = await bucket.list({ prefix, delimiter: '/' });
const files = list.objects.map((o) => o.key.replace(prefix, ''));
const dirs = (list.delimitedPrefixes || []).map((p) =>
p.replace(prefix, '').replace('/', '')
);
return [...files, ...dirs];
},
async unlink(path: string): Promise<void> {
await bucket.delete(path);
},
async mkdir(): Promise<void> {
// No-op for R2
},
async stat(path: string): Promise<{ isFile: () => boolean; isDirectory: () => boolean }> {
const head = await bucket.head(path);
return {
isFile: () => head !== null,
isDirectory: () => false,
};
},
};
}
// ============================================
// HTTP CLIENT (http/https → fetch)
// ============================================
interface HttpRequestOptions {
hostname?: string;
host?: string;
port?: number;
path?: string;
method?: string;
headers?: Record<string, string>;
timeout?: number;
}
/**
* Simple http.request replacement using fetch
*/
export async function httpRequest(
options: HttpRequestOptions | string,
body?: string | object
): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
let url: string;
let method = 'GET';
let headers: Record<string, string> = {};
let timeout: number | undefined;
if (typeof options === 'string') {
url = options;
} else {
const protocol = options.port === 443 ? 'https' : 'http';
const host = options.hostname || options.host || 'localhost';
const port = options.port ? `:${options.port}` : '';
const path = options.path || '/';
url = `${protocol}://${host}${port}${path}`;
method = options.method || 'GET';
headers = options.headers || {};
timeout = options.timeout;
}
const controller = new AbortController();
const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
try {
const response = await fetch(url, {
method,
headers,
body: body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined,
signal: controller.signal,
});
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
return {
statusCode: response.status,
headers: responseHeaders,
body: await response.text(),
};
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}
// ============================================
// CRYPTO POLYFILLS
// ============================================
/**
* crypto.randomBytes replacement
*/
export function randomBytes(size: number): Uint8Array {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
/**
* crypto.randomUUID replacement (already in Web Crypto)
*/
export function randomUUID(): string {
return crypto.randomUUID();
}
/**
* Simple hash function using Web Crypto
*/
export async function createHash(
algorithm: 'sha256' | 'sha384' | 'sha512',
data: string | Uint8Array
): Promise<string> {
const algMap = {
sha256: 'SHA-256',
sha384: 'SHA-384',
sha512: 'SHA-512',
};
const input = typeof data === 'string' ? new TextEncoder().encode(data) : data;
const hash = await crypto.subtle.digest(algMap[algorithm], input);
return Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* HMAC using Web Crypto
*/
export async function createHmac(
algorithm: 'sha256' | 'sha384' | 'sha512',
key: string,
data: string
): Promise<string> {
const algMap = {
sha256: 'SHA-256',
sha384: 'SHA-384',
sha512: 'SHA-512',
};
const keyData = new TextEncoder().encode(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: algMap[algorithm] },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
cryptoKey,
new TextEncoder().encode(data)
);
return Array.from(new Uint8Array(signature))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
// ============================================
// TIMERS
// ============================================
/**
* setTimeout that returns a Promise
*/
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* setImmediate replacement using microtask
*/
export function setImmediate(callback: () => void): void {
queueMicrotask(callback);
}
// ============================================
// BUFFER UTILITIES
// ============================================
/**
* Buffer.from replacement for common cases
*/
export function bufferFrom(
input: string | ArrayBuffer | Uint8Array,
encoding?: 'utf-8' | 'base64' | 'hex'
): Uint8Array {
if (input instanceof Uint8Array) {
return input;
}
if (input instanceof ArrayBuffer) {
return new Uint8Array(input);
}
if (encoding === 'base64') {
const binary = atob(input);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
if (encoding === 'hex') {
const bytes = new Uint8Array(input.length / 2);
for (let i = 0; i < input.length; i += 2) {
bytes[i / 2] = parseInt(input.slice(i, i + 2), 16);
}
return bytes;
}
// Default: UTF-8
return new TextEncoder().encode(input);
}
/**
* Buffer.toString replacement
*/
export function bufferToString(
buffer: Uint8Array,
encoding?: 'utf-8' | 'base64' | 'hex'
): string {
if (encoding === 'base64') {
return btoa(String.fromCharCode(...buffer));
}
if (encoding === 'hex') {
return Array.from(buffer)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
// Default: UTF-8
return new TextDecoder().decode(buffer);
}
// ============================================
// PATH UTILITIES (already supported, but included for reference)
// ============================================
export const path = {
join(...paths: string[]): string {
return paths
.join('/')
.replace(/\/+/g, '/')
.replace(/\/$/, '');
},
basename(path: string, ext?: string): string {
const base = path.split('/').pop() || '';
if (ext && base.endsWith(ext)) {
return base.slice(0, -ext.length);
}
return base;
},
dirname(path: string): string {
const parts = path.split('/');
parts.pop();
return parts.join('/') || '.';
},
extname(path: string): string {
const base = path.split('/').pop() || '';
const dotIndex = base.lastIndexOf('.');
return dotIndex > 0 ? base.slice(dotIndex) : '';
},
normalize(path: string): string {
const parts = path.split('/');
const result: string[] = [];
for (const part of parts) {
if (part === '..') {
result.pop();
} else if (part !== '.' && part !== '') {
result.push(part);
}
}
return (path.startsWith('/') ? '/' : '') + result.join('/');
},
};
// ============================================
// EVENT EMITTER (simple implementation)
// ============================================
type EventListener = (...args: any[]) => void;
export class EventEmitter {
private events = new Map<string, EventListener[]>();
on(event: string, listener: EventListener): this {
const listeners = this.events.get(event) || [];
listeners.push(listener);
this.events.set(event, listeners);
return this;
}
once(event: string, listener: EventListener): this {
const onceListener: EventListener = (...args) => {
this.off(event, onceListener);
listener(...args);
};
return this.on(event, onceListener);
}
off(event: string, listener: EventListener): this {
const listeners = this.events.get(event) || [];
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
return this;
}
emit(event: string, ...args: any[]): boolean {
const listeners = this.events.get(event) || [];
listeners.forEach((listener) => listener(...args));
return listeners.length > 0;
}
removeAllListeners(event?: string): this {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
return this;
}
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
// Before (Node.js)
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const config = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8');
const hash = crypto.createHash('sha256').update('data').digest('hex');
const apiKey = process.env.API_KEY;
// After (Workers)
import {
createFsAdapterKV,
createHash,
createProcessEnv,
path
} from './node-compat';
interface Env {
CONFIG: KVNamespace;
API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const fs = createFsAdapterKV(env.CONFIG);
const processEnv = createProcessEnv(env);
const config = await fs.readFile('config.json');
const hash = await createHash('sha256', 'data');
const apiKey = processEnv.API_KEY;
return Response.json({ config, hash });
},
};
*/
/**
* Vercel API Routes to Cloudflare Workers Adapter
*
* Enables migration of Vercel API routes with minimal changes.
* Converts NextApiRequest/NextApiResponse to Web APIs.
*
* Usage:
* 1. Keep existing Vercel API handler logic
* 2. Import and wrap with adaptVercelHandler
* 3. Export as Workers default or use with Hono
*/
// ============================================
// TYPE DEFINITIONS
// ============================================
export interface NextApiRequest {
method: string;
url: string;
headers: Record<string, string | string[] | undefined>;
query: Record<string, string | string[]>;
body: any;
cookies: Record<string, string>;
env: Record<string, string>;
}
export interface NextApiResponse<T = any> {
status(code: number): NextApiResponse<T>;
setHeader(name: string, value: string | string[]): NextApiResponse<T>;
setPreviewData(data: object): NextApiResponse<T>;
clearPreviewData(): NextApiResponse<T>;
json(body: T): void;
send(body: string): void;
redirect(url: string): NextApiResponse<T>;
redirect(status: number, url: string): NextApiResponse<T>;
revalidate(path: string): Promise<void>;
end(): void;
}
export type NextApiHandler<T = any> = (
req: NextApiRequest,
res: NextApiResponse<T>
) => void | Promise<void>;
// ============================================
// REQUEST IMPLEMENTATION
// ============================================
class WorkersNextApiRequest implements NextApiRequest {
method: string;
url: string;
headers: Record<string, string | undefined>;
query: Record<string, string | string[]>;
body: any;
cookies: Record<string, string>;
env: Record<string, string>;
constructor(request: Request, body: any, workerEnv: Record<string, any>) {
const url = new URL(request.url);
this.method = request.method;
this.url = request.url;
this.body = body;
// Convert headers
this.headers = {};
request.headers.forEach((value, key) => {
this.headers[key.toLowerCase()] = value;
});
// Parse query string
this.query = {};
url.searchParams.forEach((value, key) => {
const existing = this.query[key];
if (existing) {
if (Array.isArray(existing)) {
existing.push(value);
} else {
this.query[key] = [existing, value];
}
} else {
this.query[key] = value;
}
});
// Parse cookies
this.cookies = {};
const cookieHeader = request.headers.get('cookie');
if (cookieHeader) {
cookieHeader.split(';').forEach((cookie) => {
const [name, ...rest] = cookie.trim().split('=');
if (name) {
this.cookies[name] = rest.join('=');
}
});
}
// Convert env (strings only)
this.env = {};
Object.entries(workerEnv).forEach(([key, value]) => {
if (typeof value === 'string') {
this.env[key] = value;
}
});
}
}
// ============================================
// RESPONSE IMPLEMENTATION
// ============================================
class WorkersNextApiResponse<T = any> implements NextApiResponse<T> {
private _statusCode = 200;
private _headers = new Headers();
private _body: any = null;
private _ended = false;
private _revalidations: string[] = [];
status(code: number): NextApiResponse<T> {
this._statusCode = code;
return this;
}
setHeader(name: string, value: string | string[]): NextApiResponse<T> {
if (Array.isArray(value)) {
value.forEach((v) => this._headers.append(name, v));
} else {
this._headers.set(name, value);
}
return this;
}
setPreviewData(data: object): NextApiResponse<T> {
// Preview mode not directly supported in Workers
// Could implement with KV storage if needed
console.warn('setPreviewData not fully supported in Workers');
return this;
}
clearPreviewData(): NextApiResponse<T> {
console.warn('clearPreviewData not fully supported in Workers');
return this;
}
json(body: T): void {
this._headers.set('Content-Type', 'application/json');
this._body = JSON.stringify(body);
this._ended = true;
}
send(body: string): void {
this._body = body;
this._ended = true;
}
redirect(statusOrUrl: number | string, url?: string): NextApiResponse<T> {
if (typeof statusOrUrl === 'number') {
this._statusCode = statusOrUrl;
this._headers.set('Location', url!);
} else {
this._statusCode = 307;
this._headers.set('Location', statusOrUrl);
}
this._ended = true;
return this;
}
async revalidate(path: string): Promise<void> {
this._revalidations.push(path);
// Implement actual revalidation via cache purge
}
end(): void {
this._ended = true;
}
toResponse(): Response {
return new Response(this._body, {
status: this._statusCode,
headers: this._headers,
});
}
getRevalidations(): string[] {
return this._revalidations;
}
}
// ============================================
// ADAPTER
// ============================================
interface AdapterOptions {
/**
* Environment bindings to pass to handler
*/
env?: Record<string, any>;
/**
* Custom body parser (default: auto-detect JSON)
*/
parseBody?: (request: Request) => Promise<any>;
/**
* Cache revalidation callback
*/
onRevalidate?: (paths: string[], env: any) => Promise<void>;
}
/**
* Adapt a Vercel API handler for Cloudflare Workers
*
* @example
* // Existing Vercel handler
* export default async function handler(req, res) {
* res.status(200).json({ hello: 'world' });
* }
*
* // Workers entry point
* import { adaptVercelHandler } from './vercel-adapter';
* import handler from './api/hello';
*
* export default adaptVercelHandler(handler);
*/
export function adaptVercelHandler<T = any>(
handler: NextApiHandler<T>,
options: AdapterOptions = {}
) {
return {
async fetch(request: Request, env: Record<string, any>): Promise<Response> {
try {
// Parse body
let body: any = null;
if (request.method !== 'GET' && request.method !== 'HEAD') {
if (options.parseBody) {
body = await options.parseBody(request);
} else {
const contentType = request.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
body = await request.json();
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const text = await request.text();
body = Object.fromEntries(new URLSearchParams(text));
} else {
body = await request.text();
}
}
}
// Create request/response objects
const req = new WorkersNextApiRequest(
request,
body,
options.env || env
);
const res = new WorkersNextApiResponse<T>();
// Call handler
await handler(req, res);
// Handle revalidations
const revalidations = res.getRevalidations();
if (revalidations.length > 0 && options.onRevalidate) {
await options.onRevalidate(revalidations, env);
}
return res.toResponse();
} catch (error) {
console.error('Vercel adapter error:', error);
return new Response(
JSON.stringify({
error: 'Internal Server Error',
message: error instanceof Error ? error.message : 'Unknown error',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
},
};
}
// ============================================
// HONO INTEGRATION
// ============================================
import { Context, Hono } from 'hono';
/**
* Adapt Vercel handler for use with Hono
*/
export function adaptVercelHandlerForHono<T = any>(
handler: NextApiHandler<T>,
options: AdapterOptions = {}
) {
return async (c: Context): Promise<Response> => {
const worker = adaptVercelHandler(handler, options);
return worker.fetch(c.req.raw, c.env);
};
}
// ============================================
// EDGE MIDDLEWARE ADAPTER
// ============================================
export interface NextRequest {
url: string;
method: string;
headers: Headers;
cookies: {
get(name: string): { value: string } | undefined;
getAll(): Array<{ name: string; value: string }>;
};
nextUrl: URL;
ip?: string;
geo?: {
city?: string;
country?: string;
region?: string;
};
}
export interface NextResponse {
next(): Response;
redirect(url: URL | string, status?: number): Response;
rewrite(url: URL | string): Response;
json(body: any, init?: ResponseInit): Response;
}
type MiddlewareHandler = (request: NextRequest) => Response | Promise<Response>;
/**
* Adapt Vercel Edge Middleware for Workers
*/
export function adaptVercelMiddleware(handler: MiddlewareHandler) {
return {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Parse cookies
const cookieHeader = request.headers.get('cookie') || '';
const cookies = new Map<string, string>();
cookieHeader.split(';').forEach((cookie) => {
const [name, value] = cookie.trim().split('=');
if (name) cookies.set(name, value || '');
});
const nextRequest: NextRequest = {
url: request.url,
method: request.method,
headers: request.headers,
cookies: {
get(name: string) {
const value = cookies.get(name);
return value !== undefined ? { value } : undefined;
},
getAll() {
return Array.from(cookies.entries()).map(([name, value]) => ({
name,
value,
}));
},
},
nextUrl: url,
ip: request.headers.get('cf-connecting-ip') || undefined,
geo: {
city: request.headers.get('cf-ipcity') || undefined,
country: request.headers.get('cf-ipcountry') || undefined,
region: request.headers.get('cf-region') || undefined,
},
};
return handler(nextRequest);
},
};
}
// ============================================
// NEXT RESPONSE HELPERS
// ============================================
export const NextResponse = {
next(): Response {
return new Response(null, { status: 200 });
},
redirect(url: URL | string, status = 307): Response {
const location = typeof url === 'string' ? url : url.toString();
return new Response(null, {
status,
headers: { Location: location },
});
},
rewrite(url: URL | string): Response {
// Implement via fetch to the new URL
const destination = typeof url === 'string' ? url : url.toString();
return fetch(destination);
},
json(body: any, init: ResponseInit = {}): Response {
const headers = new Headers(init.headers);
headers.set('Content-Type', 'application/json');
return new Response(JSON.stringify(body), {
...init,
headers,
});
},
};
// ============================================
// EXAMPLE USAGE
// ============================================
/*
// Existing Vercel API route: pages/api/users/[id].ts
export default async function handler(req, res) {
const { id } = req.query;
if (req.method === 'GET') {
const user = await db.users.findById(id);
return res.status(200).json(user);
}
if (req.method === 'PUT') {
const updated = await db.users.update(id, req.body);
return res.status(200).json(updated);
}
res.status(405).end();
}
// Workers entry point
import { Hono } from 'hono';
import { adaptVercelHandlerForHono } from './vercel-adapter';
import usersHandler from './pages/api/users/[id]';
const app = new Hono();
app.all('/api/users/:id', adaptVercelHandlerForHono(usersHandler));
export default app;
// Or direct export
import { adaptVercelHandler } from './vercel-adapter';
import handler from './pages/api/hello';
export default adaptVercelHandler(handler);
*/
/*
// Existing Vercel middleware: middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// Workers migration
import { adaptVercelMiddleware, NextResponse } from './vercel-adapter';
const middleware = (request) => {
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
};
export default adaptVercelMiddleware(middleware);
*/