Routes
Define type-safe HTTP routes with Zod validation.
Routes
Ploy Start provides a type-safe routing system with automatic Zod validation for params, query strings, request bodies, and responses.
Basic Routes
Define routes using HTTP method shortcuts:
import { ploy, z } from "@meetploy/start";
const worker = ploy()
.get(
"/hello",
{
response: z.object({ message: z.string() }),
},
async () => {
return { message: "Hello World!" };
},
)
.build();Route Methods
All standard HTTP methods are supported:
const worker = ploy()
.get("/users", {...}, handler) // GET request
.post("/users", {...}, handler) // POST request
.put("/users/:id", {...}, handler) // PUT request
.patch("/users/:id", {...}, handler) // PATCH request
.delete("/users/:id", {...}, handler) // DELETE request
.build();Path Parameters
Define path parameters with :param syntax:
.get("/users/:id", {
params: z.object({
id: z.string()
}),
response: z.object({
user: z.object({
id: z.string(),
name: z.string()
}).nullable()
})
}, async (ctx) => {
// ctx.params.id is typed as string
const userId = ctx.params.id;
return { user: await findUser(userId) };
})Multiple path parameters:
.get("/orgs/:orgId/users/:userId", {
params: z.object({
orgId: z.string(),
userId: z.string()
}),
response: z.object({ user: userSchema })
}, async (ctx) => {
// ctx.params.orgId and ctx.params.userId are both typed
return { user: await findOrgUser(ctx.params.orgId, ctx.params.userId) };
})Query Parameters
Validate query strings with Zod:
.get("/users", {
query: z.object({
page: z.string().optional().default("1"),
limit: z.string().optional().default("10"),
search: z.string().optional()
}),
response: z.object({
users: z.array(userSchema),
total: z.number()
})
}, async (ctx) => {
const { page, limit, search } = ctx.query;
// All query params are typed
return { users: [], total: 0 };
})Query parameters are always strings from the URL. Use .transform() to convert them:
query: z.object({
page: z.string().transform(Number).default("1")
})Request Body
Handle JSON request bodies on POST, PUT, and PATCH:
.post("/users", {
body: z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().optional()
}),
response: z.object({
success: z.boolean(),
user: userSchema
})
}, async (ctx) => {
// ctx.body is fully typed
const { name, email, age } = ctx.body;
const user = await createUser({ name, email, age });
return { success: true, user };
})Response Schema
Every route requires a response schema for type safety and OpenAPI generation:
.get("/health", {
response: z.object({
status: z.enum(["ok", "degraded", "down"]),
timestamp: z.string()
})
}, async () => {
return {
status: "ok",
timestamp: new Date().toISOString()
};
})Route Context
Every handler receives a typed context object:
interface RouteContext<Env, State, Params, Query, Body> {
request: Request; // Raw Cloudflare Request
env: Env; // Environment bindings
ctx: ExecutionContext; // Cloudflare ExecutionContext
state: State; // State from .state() calls
params: Params; // Validated path parameters
query: Query; // Validated query parameters
body: Body; // Validated request body
json: (data, status?) => Response;
text: (data, status?) => Response;
html: (data, status?) => Response;
redirect: (url, status?) => Response;
}Response Helpers
.get("/example", {...}, async (ctx) => {
// Return JSON (default)
return { data: "value" };
// Or use helpers for other response types
return ctx.json({ data: "value" }, 201);
return ctx.text("Plain text response");
return ctx.html("<h1>HTML Response</h1>");
return ctx.redirect("/other-page", 302);
})Accessing Environment
Access Cloudflare bindings via ctx.env:
import { ploy, z } from "@meetploy/start";
import type { Env } from "./env.js";
const worker = ploy<Env>()
.get(
"/send",
{
response: z.object({ messageId: z.string() }),
},
async (ctx) => {
// Access queue binding directly
const { messageId } = await ctx.env.TASKS.send({ task: "process" });
return { messageId };
},
)
.build();OpenAPI Documentation
Enable automatic OpenAPI spec generation:
const worker = ploy()
.openapi({
path: "/docs",
info: {
title: "My API",
version: "1.0.0",
description: "API description",
},
})
.get(
"/users",
{
summary: "List all users",
description: "Returns a paginated list of users",
tags: ["Users"],
response: z.object({ users: z.array(userSchema) }),
},
handler,
)
.build();Access the OpenAPI spec at /docs and Swagger UI at /docs/ui.
Middleware
Add global middleware with .use():
import { cors, logger, auth } from "@meetploy/start";
const worker = ploy()
.use(cors())
.use(logger())
.use(auth("Authorization"))
.get("/protected", {...}, handler)
.build();Built-in Middleware
// CORS - Handle cross-origin requests
cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"],
});
// Logger - Log requests
logger();
// Auth - Extract auth header
auth("Authorization");
// Rate Limit - Basic rate limiting
rateLimit({ maxRequests: 100, windowMs: 60000 });Custom Middleware
const worker = ploy()
.use(async (ctx, next) => {
const start = Date.now();
const response = await next();
const duration = Date.now() - start;
console.log(`${ctx.request.method} ${ctx.request.url} - ${duration}ms`);
return response;
})
.build();State Management
Add shared state with .state():
import { withDrizzle } from "@meetploy/start";
const worker = ploy<Env>()
.state(withDrizzle("DB", schema))
.state((ctx) => ({
requestId: crypto.randomUUID()
}))
.get("/users", {...}, async (ctx) => {
// ctx.state.db from withDrizzle
// ctx.state.requestId from custom state
const users = await ctx.state.db.select().from(schema.users);
return { users, requestId: ctx.state.requestId };
})
.build();State factories receive { env, request } and can be async:
.state(async ({ env, request }) => {
const user = await validateToken(request.headers.get("Authorization"));
return { user };
})Error Handling
Errors thrown in handlers are caught and returned as 500 responses:
.get("/error", {...}, async () => {
throw new Error("Something went wrong");
// Returns: { error: "Something went wrong" } with status 500
})For custom error responses, return them directly:
.get("/users/:id", {...}, async (ctx) => {
const user = await findUser(ctx.params.id);
if (!user) {
return ctx.json({ error: "User not found" }, 404);
}
return { user };
})How is this guide?
Last updated on