Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

Deploy Node.js & Express APIs to Cloudflare Workers: A Practical Command Cheat Sheet

A quick reference for deploying a Node.js / Express API to Cloudflare Workers. Each section has a short note on what it does, then the commands. Commands marked LIVE change the real running service. Everything else is

A quick reference for deploying a Node.js / Express API to Cloudflare Workers. Each section has a short note on what it does, then the commands.

Commands marked LIVE change the real running service. Everything else is safe to run.

Replace my-worker, my-account, and example.com with your own values.

Setup

Install dependencies and log in to Cloudflare. Login only connects the CLI to your account. It does not deploy anything.

node --version
pnpm --version
pnpm install
pnpm add -D wrangler               # if the project doesn't have it yet
pnpm exec wrangler login
pnpm exec wrangler whoami          # confirm the right account

Minimal Express Worker

The smallest working example. wrangler.jsonc tells Cloudflare where your code is, and src/index.ts wraps Express so Workers can run it.

wrangler.jsonc

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-10-29",
  "compatibility_flags": ["nodejs_compat"]
}

src/index.ts

import { httpServerHandler } from 'cloudflare:node';
import express from 'express';

const app = express();
app.use(express.json({ limit: '1mb' }));

app.get('/health', (_req, res) => res.json({ status: 'ok' }));

app.listen(3000);
export default httpServerHandler({ port: 3000 });

The port is only an internal routing detail, not a public port.

Local run + check

Run the Worker on your machine and check that it builds. A dry run proves bundling works, but not that login, database or uploads work.

pnpm exec wrangler types
pnpm exec wrangler dev
pnpm exec tsc --noEmit
pnpm exec wrangler deploy --dry-run   # bundle check only

Deploy (LIVE)

Publishes your Worker. Wrangler prints the public URL when it finishes.

pnpm exec wrangler deploy

Secrets (LIVE)

Secrets (database URL, API keys) are stored in Cloudflare, not in your code. Editing a local file does not change the live Worker. Set the secret, then redeploy if needed.

pnpm exec wrangler secret put MONGODB_URI
pnpm exec wrangler secret list

For local development, put values in .dev.vars and keep it out of git:

MONGODB_URI=<your-connection-string>
JWT_SECRET=<your-signing-secret>

Never put secrets in NEXT_PUBLIC_* variables. They are exposed to the browser.

Verify

After deploying, test the live URL. health means the app is up. A ready endpoint (if you build one) can also check the database. The CORS test shows whether your frontend origin is allowed.

curl --fail-with-body https://my-worker.my-account.workers.dev/health

# CORS preflight (try an allowed AND a disallowed origin)
curl -i -X OPTIONS https://my-worker.my-account.workers.dev/api/upload \
  -H 'Origin: http://localhost:3000' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type'

Database note

Workers can't hold a TCP connection across requests like a normal server.

  • MongoDB: use the native driver or Mongoose 9+, and keep the connection inside a Durable Object.
  • SQL: use D1, or Hyperdrive for Postgres/MySQL.
  • localhost in your database URL means Cloudflare's servers, not your laptop. Use a cloud URL.

Logs / history

See live errors and past deployments. Redact tokens and cookies before sharing logs.

pnpm exec wrangler tail
pnpm exec wrangler deployments list
pnpm exec wrangler versions list

Rollback (LIVE)

Goes back to an older Worker version. It does not undo database writes or uploaded files.

pnpm exec wrangler rollback VERSION_ID --message "Restore known-good version"

Staging (needs env.staging in wrangler.jsonc first)

A separate Worker for testing. Use separate secrets and a separate database, or staging can write to production data.

pnpm exec wrangler dev --env staging
pnpm exec wrangler deploy --env staging   # LIVE

CORS and cookies

CORS decides which browser origins may call your API. Origins include scheme and port, with no path and no trailing slash.

CORS_ORIGIN=http://localhost:3000,https://app.example.com
  • With cookies, return the exact origin plus Access-Control-Allow-Credentials: true. Never use *.
  • Local frontend calling a workers.dev API is cross-site, so cookies need Secure + SameSite=None.
  • Browsers can still block third-party cookies even when CORS is correct. Check the Network and Application tabs.

Frontend still calling localhost?

NEXT_PUBLIC_* values are baked into the frontend at build time. Change the file, then restart the dev server (or rebuild for production).

# .env.local
NEXT_PUBLIC_API_URL=https://my-worker.my-account.workers.dev

# then restart
pnpm dev

Domain Name

Your Worker gets a free workers.dev address automatically. You can also add your own domain, or use both at once.

Option 1: workers.dev (no DNS needed)

Works right after deploy. No A record and no DNS change.

https://<worker-name>.<account-subdomain>.workers.dev
https://my-worker.my-account.workers.dev

Option 2: Custom domain (e.g. api.example.com)

Gives you a professional address with automatic HTTPS. The domain must be managed by Cloudflare DNS.

Step 1: Add the domain to Cloudflare (Dashboard > Add a site), then copy the 2 nameservers it gives you.

Step 2: Copy old DNS records first (A, AAAA, MX, TXT, CNAME) into Cloudflare so email and website don't break.

Step 3: Change nameservers at your registrar to the ones Cloudflare shows you.

Step 4: Check the zone is active

dig NS example.com +short

Step 5: Attach the hostname to the Worker in wrangler.jsonc

{
  "name": "my-worker",
  "main": "src/index.ts",
  "routes": [
    { "pattern": "api.example.com", "custom_domain": true }
  ]
}
pnpm exec wrangler deploy      # LIVE

(Or Dashboard > Worker > Settings > Domains & Routes > Add > Custom Domain.)

Step 6: Verify

dig api.example.com +short
curl -I https://api.example.com/health

Option 3: DNS stays at another provider

Custom Domains need the zone on Cloudflare. If DNS stays elsewhere, move the zone or keep using workers.dev.

Use BOTH together (workers.dev + custom domain)

Both URLs point to the same Worker at the same time. Good for testing on workers.dev while production uses your domain.

{
  "name": "my-worker",
  "main": "src/index.ts",
  "workers_dev": true,
  "routes": [
    { "pattern": "api.example.com", "custom_domain": true }
  ]
}
pnpm exec wrangler deploy      # LIVE
# test both
curl -I https://my-worker.my-account.workers.dev/health
curl -I https://api.example.com/health

Notes:

  • If you use Google login, add BOTH callback URLs in Google Cloud Console:
  https://my-worker.my-account.workers.dev/auth/google/callback
  https://api.example.com/auth/google/callback
  • Your app uses one callback URL at a time. Set it to your main one (usually the custom domain).
  • Cookies are host-only, so a login on one hostname does not work on the other.
  • To turn workers.dev off later: set "workers_dev": false and deploy.

After the domain changes, update everything that uses the URL

The domain alone does nothing for your app settings. CORS, cookies, OAuth and frontends all need the new URL.

BACKEND_URL=https://api.example.com
CORS_ORIGIN=https://example.com,https://admin.example.com
GOOGLE_CALLBACK_URL=https://api.example.com/auth/google/callback
COOKIE_DOMAIN=            # leave empty = host-only cookie
COOKIE_SAME_SITE=lax      # ok when frontend and API share the same site
# frontend .env.local, then rebuild
NEXT_PUBLIC_API_URL=https://api.example.com
  • Add the new callback URL in Google Cloud Console (OAuth client > Authorized redirect URIs).
  • Redeploy: pnpm exec wrangler deploy (LIVE).
  • Rebuild frontends (NEXT_PUBLIC_* is baked in at build time).

Domain troubleshooting

Problem Check
404 on new domain Custom domain not attached; check Domains & Routes
SSL error right after setup Wait a few minutes for the certificate; retry
Site/email broke after nameserver change Missing copied DNS records (A, MX, TXT)
redirect_uri_mismatch New callback not added in Google Console
CORS error on new domain CORS_ORIGIN still has old origin; redeploy

Common errors

Error Likely cause
Cloudflare 1101 Unhandled exception; read wrangler tail
Cloudflare 1102 CPU or memory limit exceeded
__dirname is not defined Package expects local files; bundle assets or replace it
[unenv] ... not implemented Node API not supported in Workers
ECONNREFUSED 127.0.0.1 A localhost database URL was deployed
Disallowed operation in global scope I/O, timers or randomness at import time; move into a request

Limits to know

Resource Value
Memory per isolate 128 MB
Free plan CPU per request 10 ms
Paid plan CPU per request 30 s default, up to 5 min
Simultaneous outgoing connections 6

Limits change, so check the current Cloudflare limits.

CI deploys

For automated deploys (GitHub Actions etc.), use an API token instead of wrangler login. Store these in your CI's protected secrets, never in git.

CLOUDFLARE_API_TOKEN=<token>
CLOUDFLARE_ACCOUNT_ID=<id>

References

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.