Skip to main content

Create your first worker

This guide will walk you through creating and running your first workerd application.
1

Install workerd

Install workerd using npm:
npm install -g workerd
Or run it directly with npx:
npx workerd --version
See the installation guide for other installation methods and system requirements.
2

Create a worker script

Create a new directory for your project and a worker file:
mkdir my-first-worker
cd my-first-worker
Create worker.js with a simple HTTP handler:
worker.js
// Export a default object with a fetch handler
export default {
  async fetch(request, env) {
    return new Response("Hello World\n", {
      headers: { "Content-Type": "text/plain" },
    });
  }
};
This worker responds to all HTTP requests with “Hello World”.
3

Create a configuration file

Create config.capnp to configure workerd:
config.capnp
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "main", worker = .mainWorker),
  ],

  sockets = [
    # Serve HTTP on port 8080
    ( name = "http",
      address = "*:8080",
      http = (),
      service = "main"
    ),
  ]
);

const mainWorker :Workerd.Worker = (
  modules = [
    (name = "worker", esModule = embed "worker.js")
  ],
  compatibilityDate = "2024-01-01",
  # Learn more about compatibility dates:
  # https://developers.cloudflare.com/workers/platform/compatibility-dates/
);
This configuration:
  • Defines a worker service named “main”
  • Embeds your worker.js file
  • Listens on port 8080
  • Sets a compatibility date for API versioning
4

Run the server

Start workerd with your configuration:
workerd serve config.capnp
You should see output indicating the server is running:
Listening on http://0.0.0.0:8080
5

Test your worker

In another terminal, test your worker:
curl http://localhost:8080
You should see:
Hello World
🎉 Congratulations! You’ve successfully run your first workerd application.

Understanding the code

Worker format

workerd supports two worker formats:

Configuration structure

The Cap’n Proto configuration has three key parts:
  1. Services: Define your workers and other services
  2. Sockets: Configure HTTP/HTTPS listeners
  3. Worker definition: Specify modules, compatibility, and bindings
services = [
  (name = "main", worker = .mainWorker),
],

Next examples

Handle different routes

Add routing logic to your worker:
worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/":
        return new Response("Home page");
      case "/about":
        return new Response("About page");
      case "/api/hello":
        return Response.json({ message: "Hello from API!" });
      default:
        return new Response("Not found", { status: 404 });
    }
  }
};

Return JSON responses

Use the built-in Response.json() method:
worker.js
export default {
  async fetch(request, env) {
    const data = {
      timestamp: Date.now(),
      method: request.method,
      url: request.url,
    };

    return Response.json(data);
  }
};

Use environment bindings

Add bindings to access external resources:
config.capnp
const mainWorker :Workerd.Worker = (
  modules = [
    (name = "worker", esModule = embed "worker.js")
  ],
  compatibilityDate = "2024-01-01",
  bindings = [
    # Simple text binding
    (name = "MESSAGE", text = "Hello from environment!"),
  ],
);
Access it in your worker:
worker.js
export default {
  async fetch(request, env) {
    // Access bindings through the env parameter
    return new Response(env.MESSAGE);
  }
};

Working with multiple workers

workerd supports the nanoservices pattern - multiple workers calling each other:
config.capnp
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    (name = "frontend", worker = .frontendWorker),
    (name = "backend", worker = .backendWorker),
  ],
  sockets = [
    ( name = "http",
      address = "*:8080",
      http = (),
      service = "frontend"  # Frontend receives requests
    ),
  ]
);

const frontendWorker :Workerd.Worker = (
  modules = [(name = "worker", esModule = embed "frontend.js")],
  compatibilityDate = "2024-01-01",
  bindings = [
    # Service binding to call the backend
    (name = "backend", service = "backend"),
  ],
);

const backendWorker :Workerd.Worker = (
  modules = [(name = "worker", esModule = embed "backend.js")],
  compatibilityDate = "2024-01-01",
);
frontend.js
export default {
  async fetch(request, env) {
    // Call the backend service
    const response = await env.backend.fetch("http://backend/api");
    const data = await response.json();
    
    return Response.json({
      frontend: "processed",
      backend: data,
    });
  }
};
backend.js
export default {
  async fetch(request, env) {
    return Response.json({
      message: "Hello from backend",
      timestamp: Date.now(),
    });
  }
};
Service-to-service calls happen in the same process with no network overhead. This gives you microservices-like architecture with monolith-like performance.

Command line options

Serve command

Run a configuration file:
workerd serve config.capnp
Common options:
  • --verbose - Enable verbose logging (includes application errors)
  • --socket-fd <name>=<fd> - Inherit socket from file descriptor

Compile command

Create a standalone binary with embedded config and code:
workerd compile config.capnp -o my-server
./my-server  # Run the compiled server
This bundles everything into a single executable.

Test command

Run worker unit tests (requires test definitions in config):
workerd test config.capnp

Next steps

Core concepts

Learn about workerd’s architecture and design

Configuration guide

Master the Cap’n Proto configuration format

Runtime APIs

Explore available JavaScript APIs

Sample configs

Browse more examples in the GitHub repository

Common patterns

Enable Node.js compatibility

Use Node.js APIs by adding the compatibility flag:
config.capnp
const mainWorker :Workerd.Worker = (
  modules = [(name = "worker", esModule = embed "worker.js")],
  compatibilityDate = "2024-01-01",
  compatibilityFlags = ["nodejs_compat"],
);
worker.js
import { Buffer } from 'node:buffer';
import { EventEmitter } from 'node:events';
import path from 'node:path';

export default {
  async fetch(request, env) {
    const buffer = Buffer.from('Hello!');
    return new Response(buffer);
  }
};

Serve static files

Use the disk binding to serve files:
config.capnp
const mainWorker :Workerd.Worker = (
  modules = [(name = "worker", esModule = embed "worker.js")],
  compatibilityDate = "2024-01-01",
  bindings = [
    (name = "files", disk = "./public"),
  ],
);
worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname.slice(1) || 'index.html';
    
    try {
      const file = await env.files.fetch(`http://host/${path}`);
      return file;
    } catch (err) {
      return new Response('Not found', { status: 404 });
    }
  }
};

Handle WebSocket connections

worker.js
export default {
  async fetch(request, env) {
    // Check if this is a WebSocket upgrade request
    if (request.headers.get('Upgrade') === 'websocket') {
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);

      // Handle messages
      server.accept();
      server.addEventListener('message', event => {
        server.send(`Echo: ${event.data}`);
      });

      return new Response(null, {
        status: 101,
        webSocket: client,
      });
    }

    return new Response('Expected WebSocket', { status: 400 });
  }
};
For production deployments, see the systemd deployment guide.

Build docs developers (and LLMs) love