Cloudflare Workers: From Zero to Production API in One Afternoon
Build14 min read·April 17, 2026·--

Cloudflare Workers: From Zero to Production API in One Afternoon

Build fast, globally distributed APIs without managing a server. Cloudflare Workers run at 300+ edge locations worldwide — this guide teaches you everything from hello world to production.

@
@kivorablog
April 17, 2026
Share

What Cloudflare Workers Actually Are


Cloudflare Workers are JavaScript functions that run at Cloudflare's edge network — meaning your code executes at a data centre physically close to your user, not on a server you manage. A request from Lagos hits a Lagos data centre. A request from London hits a London data centre. Response times under 50ms globally.


The free tier is genuinely useful:


PlanRequestsCPU TimePrice
Free100,000/day10ms/request$0
Paid10,000,000/month50ms/request$5/month
EnterpriseUnlimitedCustomCustom

For most side projects and early-stage products, the free tier never runs out.




When to Use Workers vs Next.js API Routes


Use CaseBest ChoiceWhy
API that needs global low latencyWorkersEdge execution worldwide
Full-stack app with React frontendNext.js on Cloudflare PagesCo-located with your UI
High-volume webhook receiverWorkersHandles scale natively
Complex server logic with long runtimeTraditional serverWorkers have CPU time limits
Image or asset transformationWorkers + R2Native Cloudflare integration
Simple REST APIWorkersSimplest, fastest deployment



Step 1: Install Wrangler and Create Your First Worker


npm install -g wrangler
wrangler login
wrangler init my-api
cd my-api

This creates a project with:


my-api/
├── src/
│   └── index.js
├── wrangler.toml
└── package.json

Your First Worker


// src/index.js
export default {
  async fetch(request, env, ctx) {
    const url    = new URL(request.url)
    const path   = url.pathname

    // Simple router
    if (path === '/api/hello' && request.method === 'GET') {
      return Response.json({ message: 'Hello from the edge!', region: request.cf?.country })
    }

    if (path === '/api/echo' && request.method === 'POST') {
      const body = await request.json()
      return Response.json({ received: body, timestamp: Date.now() })
    }

    return new Response('Not Found', { status: 404 })
  }
},

Test it locally:


wrangler dev
# Open http://localhost:8787/api/hello



Step 2: Add a Database With Cloudflare D1


D1 is Cloudflare's edge SQLite database. It runs at the edge alongside your Worker.


Create the Database


wrangler d1 create my-database

Copy the database ID output and add it to wrangler.toml:


[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id-here"

Create Your Schema


# Create a migration file
wrangler d1 execute my-database --local --command "
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT,
    created_at TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER REFERENCES users(id),
    title TEXT NOT NULL,
    content TEXT,
    created_at TEXT DEFAULT (datetime('now'))
  );
"

Use D1 in Your Worker


export default {
  async fetch(request, env, ctx) {
    const url  = new URL(request.url)
    const path = url.pathname

    // GET /api/users — list all users
    if (path === '/api/users' && request.method === 'GET') {
      const { results } = await env.DB.prepare(
        'SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT 50'
      ).all()

      return Response.json({ users: results })
    }

    // POST /api/users — create a user
    if (path === '/api/users' && request.method === 'POST') {
      const { email, name } = await request.json()

      if (!email) {
        return Response.json({ error: 'Email required' }, { status: 400 })
      }

      try {
        const result = await env.DB.prepare(
          'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *'
        ).bind(email, name || null).first()

        return Response.json({ user: result }, { status: 201 })
      } catch (e) {
        if (e.message.includes('UNIQUE constraint')) {
          return Response.json({ error: 'Email already exists' }, { status: 409 })
        }
        throw e
      }
    }

    return new Response('Not Found', { status: 404 })
  }
},



Step 3: Add KV Storage for Caching


Cloudflare KV (Key-Value) is a globally replicated store. Perfect for caching expensive computations, storing session data, or rate limiting.


Add KV to wrangler.toml


[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"

Create the namespace:


wrangler kv:namespace create CACHE

Cache Expensive API Calls


async function getCachedOrFetch(env, key, fetchFn, ttlSeconds = 300) {
  // Try cache first
  const cached = await env.CACHE.get(key, { type: 'json' })
  if (cached) return cached

  // Cache miss — fetch and store
  const fresh = await fetchFn()
  await env.CACHE.put(key, JSON.stringify(fresh), { expirationTtl: ttlSeconds })
  return fresh
},

// Usage in your handler
const data = await getCachedOrFetch(
  env,
  'exchange-rates-usd',
  () => fetch('https://api.exchangerate-api.com/v4/latest/USD').then(r => r.json()),
  3600  // Cache for 1 hour
)



Step 4: Add Authentication


Workers don't have sessions, but they work perfectly with JWT tokens:


async function verifyToken(token, secret) {
  // Simple JWT verification using WebCrypto API (available in Workers)
  const parts = token.split('.')
  if (parts.length !== 3) return null

  try {
    const payload = JSON.parse(atob(parts[1]))
    if (payload.exp && payload.exp < Date.now() / 1000) return null
    return payload
  } catch {
    return null
  }
},

function requireAuth(handler) {
  return async (request, env, ctx) => {
    const authHeader = request.headers.get('Authorization')
    if (!authHeader?.startsWith('Bearer ')) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const token = authHeader.slice(7)
    const payload = await verifyToken(token, env.JWT_SECRET)

    if (!payload) {
      return Response.json({ error: 'Invalid token' }, { status: 401 })
    }

    // Attach user to request context
    request.user = payload
    return handler(request, env, ctx)
  }
},

// Protect a route
const protectedHandler = requireAuth(async (request, env) => {
  return Response.json({ user: request.user, message: 'Authenticated!' })
})



Step 5: Deploy to Production


# Deploy to Cloudflare's edge network (300+ locations)
wrangler deploy

Your API is now live at https://my-api.your-username.workers.dev.


Add a Custom Domain


  • In Cloudflare dashboard, go to Workers & Pages → your worker
  • Click TriggersAdd Custom Domain
  • Enter api.yourdomain.com
  • Done — Cloudflare handles SSL automatically

Performance Comparison: Workers vs Traditional Servers

MetricTraditional Server (1 location)Cloudflare Workers (300+ locations)
Latency from Lagos200–400ms to EU/US server15–40ms (Lagos edge)
Latency from London20–50ms10–20ms
Cold start100–500ms~0ms (always warm)
Scale to 1M requestsNeeds server scalingAutomatic
Cost at 1M requests/month$20–$100 server$5 Workers Paid

Common Gotchas

IssueCauseFix
`Cannot use Node.js modules`Workers use Web APIs not Node.jsUse Web-compatible libraries
`CPU time limit exceeded`Heavy computationMove to a background job or use Durable Objects
`D1 returning empty results`Local vs production DB mismatchRun migrations on production: `wrangler d1 execute --remote`
`Environment variable undefined`Secrets not set in WranglerUse `wrangler secret put MY_KEY`
Read more on Kivora Blog

Read more on Kivora Blog

Get started →