tRPC-style Safe RPC methods for Cloudflare Durable Objects
npm install safe-durable-objectstRPC-style Safe RPC methods for Cloudflare Durable Objects


Safe Durable Objects brings type-safe, validated RPC methods to Cloudflare Durable Objects with a developer experience inspired by tRPC. It uses Zod for runtime validation and provides full TypeScript support.
You can access the router and schemas via YourClass.prototype._def or YourClass.prototype.route._def as you would with tRPC. This is extremely powerful as you can convert the schemas to a JSON schema and use them to convert your durable object methods into callable tools for your AI agents.
- 🔒 Type-safe: Full TypeScript support with end-to-end type safety
- ✅ Runtime validation: Input and output validation using Zod schemas
- 🎯 tRPC-inspired API: Familiar developer experience with .input(), .output(), and .implement()
``bash`
npm install safe-durable-objects zodor
pnpm add safe-durable-objects zodor
yarn add safe-durable-objects zodor
bun add safe-durable-objects zod
You'll also need @cloudflare/workers-types for TypeScript support:
`bash`
npm install -D @cloudflare/workers-types
Here's a complete example of how to use Safe Durable Objects:
`typescript
import { z } from "zod/v4";
import { SafeDurableObjectBuilder } from "safe-durable-objects";
import { DurableObject } from "cloudflare:workers";
type State = {
count: number;
lastMessage: string;
};
type Env = {
MY_DURABLE_OBJECT: DurableObjectNamespace
};
export class MyDurableObject extends SafeDurableObjectBuilder(
// This is the base class
class extends DurableObject
state: State;
// important: make sure to make the ctx and env public, else you won't be able to access them in the router and typescript will complain
constructor(public ctx: DurableObjectState, public env: Env) {
super(ctx, env);
this.state = {
count: 0,
lastMessage: "",
};
}
setState(state: State) {
this.state = state;
}
},
(fn) => ({
hello: fn
.input(z.string())
.output(z.object({ message: z.string(), id: z.string() }))
.implement(function ({ ctx, input }) {
// You can access the base class methods via thisHello, ${input}!! state: ${JSON.stringify(state)}
const state = this.state;
this.setState({
count: state.count + 1,
lastMessage: input,
});
return {
message: ,
id: ctx.id.toString(),
};
}),
ping: fn.output(z.object({ message: z.string() })).implement(function () {
return {
message: "pong",
};
}),
})
) {}
export default {
async fetch(request, env, ctx) {
const stub = env.MY_DURABLE_OBJECT.get(
env.MY_DURABLE_OBJECT.idFromName("test")
);
const res = await stub.hello("world");
return Response.json(res);
},
} as ExportedHandler
`
Creates a new Durable Object class with safe RPC methods.
#### Parameters
- BaseClass: Your base Durable Object classrouterBuilder
- : A function that receives a route builder and returns an object with your RPC methods
#### Route Builder API
The route builder provides a fluent API for defining RPC methods:
`typescript`
fn.input(inputSchema).output(outputSchema).implement(handler);
// or
fn.input(inputSchema).implement(handler); // output schema is optional
Note: Only zod/v4 schemas are supported
##### .input(schema) (optional)
Defines the input validation schema using Zod. The input will be validated at runtime.
##### .output(schema) (optional)
Defines the output validation schema using Zod. The output will be validated at runtime.
##### .implement(handler)
Implements the actual RPC method logic. The handler receives:
- ctx: The DurableObjectStateenv
- : The environment bindingsinput
- : The validated input (typed according to your input schema)
If you use a function instead of an arrow function in the implement block, you can access the base class via this
`typescript
import { z } from "zod/v4";
import { SafeDurableObjectBuilder } from "safe-durable-objects";
import { DurableObject } from "cloudflare:workers";
export class Counter extends SafeDurableObjectBuilder(
class extends DurableObject
private count = 0;
// important: make sure to make the ctx and env public, else you won't be able to access them in the router and typescript will complain
constructor(public ctx: DurableObjectState, public env: Env) {
super(ctx, env);
}
async getCount() {
return this.count;
}
async setCount(value: number) {
this.count = value;
}
},
(fn) => ({
increment: fn
.input(z.object({ by: z.number().optional().default(1) }))
.output(z.object({ count: z.number() }))
.implement(async function ({ input }) {
const currentCount = await this.getCount();
const newCount = currentCount + input.by;
await this.setCount(newCount);
return { count: newCount };
}),
getCount: fn
.input(z.void())
.output(z.object({ count: z.number() }))
.implement(async function () {
const count = await this.getCount();
return { count };
}),
})
) {}
`
Safe Durable Objects automatically handles validation errors. If input validation fails, a ZodError will be thrown. If output validation fails, it will also throw a ZodError.
`typescript``
const result = await stub.hello("invalid input").catch((error) => {
/handle here/
});
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © Iterate
If you have any questions or need help, please open an issue on GitHub.