Workers
Deploy serverless workers on Ploy with Cloudflare's workerd runtime.
Workers
Ploy supports deploying serverless workers using Cloudflare's workerd runtime. Workers are lightweight, fast, and scale automatically to handle incoming requests.
If you're migrating an existing Cloudflare Worker, Ploy is designed to let you
keep the same D1 and KV calling patterns. Match the binding names in
ploy.yaml, run ploy types, and most worker code can move over unchanged.
What are Workers?
Workers are JavaScript/TypeScript functions that run in response to HTTP requests. They execute in a V8 isolate environment, providing:
- Fast cold starts - Workers start in milliseconds
- Global distribution - Run close to your users
- Auto-scaling - Handle any amount of traffic
- Cost-effective - Scale to zero when not in use
Basic Worker Example
Here's the simplest worker that responds to all requests:
export default {
fetch() {
return new Response("hi!");
},
};This worker exports a default object with a fetch method that returns a plain text response.
Project Structure
A basic worker project requires:
my-worker/
├── src/
│ └── index.ts # Worker entry point
├── package.json # Project configuration
└── tsconfig.json # TypeScript configurationpackage.json
{
"name": "my-worker",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"build": "ploy build"
},
"dependencies": {
"@meetploy/types": "latest"
},
"devDependencies": {
"@meetploy/cli": "latest",
"typescript": "5.9.2"
}
}The "type": "module" field is required for ES modules support. The main
field should point to your entry file.
Request and Response
Handling Requests
The fetch method receives a Request object with information about the incoming request:
Ploy infers the full fetch() entrypoint signature automatically, so the docs
omit explicit Request, PloyEnv, and ExecutionContext annotations on
worker handlers.
export default {
async fetch(request) {
const url = new URL(request.url);
const path = url.pathname;
if (path === "/") {
return new Response("Home page");
}
if (path === "/about") {
return new Response("About page");
}
return new Response("Not Found", { status: 404 });
},
};Response Types
You can return different types of responses:
// Plain text
new Response("Hello World");
// JSON
new Response(JSON.stringify({ message: "Hello" }), {
headers: { "Content-Type": "application/json" },
});
// HTML
new Response("<h1>Hello World</h1>", {
headers: { "Content-Type": "text/html" },
});
// With status code
new Response("Not Found", { status: 404 });
// With headers
new Response("OK", {
headers: {
"Content-Type": "text/plain",
"Cache-Control": "max-age=3600",
},
});Background Tasks with ctx.waitUntil()
Workers on Ploy support ctx.waitUntil() with the same execution model as
Cloudflare Workers. Ploy uses workerd's native execution context, so you can
return a response immediately while background work continues until the promise
settles.
Use this for non-blocking work such as analytics, cache warming, or webhook delivery:
export default {
async fetch(request, _env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/signup") {
ctx.waitUntil(
fetch("https://example.com/webhooks/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: url.searchParams.get("email"),
}),
}),
);
return Response.json({ queued: true });
}
return new Response("ok");
},
};The client gets the response right away, but the promise passed to
ctx.waitUntil() keeps running in the background.
Environment Variables
Workers can access environment variables for configuration:
interface Env {
API_KEY: string;
DATABASE_URL: string;
}
export default {
async fetch(request, env) {
// Access environment variables
const apiKey = env.API_KEY;
await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
return new Response("OK");
},
};Set environment variables in your Ploy project settings. They'll be securely injected at runtime.
Cloudflare Compatibility
Ploy workers run on workerd, and Ploy's storage bindings now follow the
Cloudflare shapes for the most common migration targets:
dbbindings generateDatabasetypes, which stay compatible with the CloudflareD1DatabaseAPI and supportprepare,batch,exec, andwithSession.statebindings keep the same Ploy names but expose the Cloudflare KV API (get,put,delete,getWithMetadata,list) in addition to Ploy'sset()andupdate().
That means code like this generally ports directly:
export default {
async fetch(_request, env) {
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)",
);
await env.STATE.put("settings", JSON.stringify({ theme: "dark" }));
const settings = await env.STATE.get("settings", "json");
const user = await env.DB.prepare("SELECT 1 AS id, 'Ada' AS name").first<{
id: number;
name: string;
}>();
return Response.json({ user, settings });
},
};Routing
Create a simple router by parsing the URL pathname:
export default {
async fetch(request) {
const url = new URL(request.url);
// Health check endpoint
if (url.pathname === "/health") {
return new Response("ok");
}
// API endpoint
if (url.pathname === "/api/users") {
return new Response(
JSON.stringify({
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
}),
{
headers: { "Content-Type": "application/json" },
},
);
}
// Default response
return new Response("Welcome!");
},
};Query Parameters
Access URL query parameters:
export default {
async fetch(request) {
const url = new URL(request.url);
// Get query parameters
const name = url.searchParams.get("name") || "Guest";
const age = url.searchParams.get("age");
return new Response(`Hello ${name}${age ? `, age ${age}` : ""}!`);
},
};Example request: /greet?name=Alice&age=25
HTTP Methods
Handle different HTTP methods:
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/api/data") {
switch (request.method) {
case "GET":
return new Response(JSON.stringify({ data: "..." }), {
headers: { "Content-Type": "application/json" },
});
case "POST":
const body = await request.json();
return new Response(JSON.stringify({ success: true, body }), {
headers: { "Content-Type": "application/json" },
});
case "DELETE":
return new Response(null, { status: 204 });
default:
return new Response("Method Not Allowed", { status: 405 });
}
}
return new Response("Not Found", { status: 404 });
},
};Request Body
Parse different request body types:
export default {
async fetch(request) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
// Parse JSON
if (request.headers.get("Content-Type")?.includes("application/json")) {
const json = await request.json();
return new Response(JSON.stringify({ received: json }), {
headers: { "Content-Type": "application/json" },
});
}
// Parse form data
if (
request.headers
.get("Content-Type")
?.includes("application/x-www-form-urlencoded")
) {
const formData = await request.formData();
const name = formData.get("name");
return new Response(`Hello ${name}!`);
}
// Plain text
const text = await request.text();
return new Response(`Received: ${text}`);
},
};Error Handling
Add error handling to your workers:
export default {
async fetch(request) {
try {
const url = new URL(request.url);
if (url.pathname === "/error") {
throw new Error("Something went wrong!");
}
return new Response("Success");
} catch (error) {
console.error("Worker error:", error);
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
},
};TypeScript Support
Add TypeScript types for better development experience:
interface Env {
API_KEY: string;
DATABASE_URL: string;
}
interface ApiResponse {
success: boolean;
data?: unknown;
error?: string;
}
export default {
async fetch(request, env) {
const response: ApiResponse = {
success: true,
data: { message: "Hello from TypeScript!" },
};
return new Response(JSON.stringify(response), {
headers: { "Content-Type": "application/json" },
});
},
};Deployment
To deploy your worker to Ploy:
- Push your code to a GitHub repository
- Connect the repository in Ploy dashboard
- Configure project type as Dynamic
- Set environment variables if needed
- Push code to trigger deployment
Workers are automatically built and deployed when you push to your repository. Build logs are available in real-time.
Best Practices
- Keep workers lightweight - Workers should respond quickly
- Use async/await - Handle asynchronous operations properly
- Set appropriate headers - Include
Content-Typeand caching headers - Handle errors gracefully - Always wrap code in try/catch blocks
- Log important events - Use
console.log()for debugging (visible in deployment logs) - Validate input - Check query parameters and request bodies
- Return proper status codes - Use 200 for success, 404 for not found, 500 for errors
Next Steps
- AI Examples - Learn how to integrate AI models in your workers
- Configuration - Configure your Ploy project
- Self-Host - Deploy Ploy on your own infrastructure
How is this guide?
Last updated on