Create your first worker
This guide will walk you through creating and running your first workerd application.
Install workerd
Install workerd using npm: Or run it directly with npx:
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: // 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”.
Create a configuration file
Create config.capnp to configure workerd: 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
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
Test your worker
In another terminal, test your worker: curl http://localhost:8080
You should see: 🎉 Congratulations! You’ve successfully run your first workerd application.
Understanding the code
workerd supports two worker formats:
ES modules (recommended)
Service Worker syntax
export default {
async fetch ( request , env ) {
return new Response ( "Hello!" );
}
} ;
The modern format using ES modules with a default export. addEventListener ( 'fetch' , event => {
event . respondWith ( handle ( event . request ));
});
async function handle ( request ) {
return new Response ( "Hello!" );
}
The classic format using event listeners (still fully supported).
Configuration structure
The Cap’n Proto configuration has three key parts:
Services : Define your workers and other services
Sockets : Configure HTTP/HTTPS listeners
Worker definition : Specify modules, compatibility, and bindings
services = [
(name = "main", worker = .mainWorker),
],
Next examples
Handle different routes
Add routing logic to your worker:
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:
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:
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:
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:
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",
);
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 ,
});
}
} ;
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:
const mainWorker :Workerd.Worker = (
modules = [(name = "worker", esModule = embed "worker.js")],
compatibilityDate = "2024-01-01",
compatibilityFlags = ["nodejs_compat"],
);
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:
const mainWorker :Workerd.Worker = (
modules = [(name = "worker", esModule = embed "worker.js")],
compatibilityDate = "2024-01-01",
bindings = [
(name = "files", disk = "./public"),
],
);
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
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 });
}
} ;