npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

safe-durable-objects

v0.1.7

Published

tRPC-style Safe RPC methods for Cloudflare Durable Objects

Downloads

72

Readme

Safe Durable Objects

tRPC-style Safe RPC methods for Cloudflare Durable Objects

npm version License: MIT

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.

Features

  • 🔒 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()

Installation

npm install safe-durable-objects zod
# or
pnpm add safe-durable-objects zod
# or
yarn add safe-durable-objects zod
# or
bun add safe-durable-objects zod

You'll also need @cloudflare/workers-types for TypeScript support:

npm install -D @cloudflare/workers-types

Quick Start

Here's a complete example of how to use Safe Durable Objects:

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<MyDurableObject>;
};

export class MyDurableObject extends SafeDurableObjectBuilder(
  // This is the base class
  class extends DurableObject<Env> {
    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 `this`
        const state = this.state;
        this.setState({
          count: state.count + 1,
          lastMessage: input,
        });
        return {
          message: `Hello, ${input}!! state: ${JSON.stringify(state)}`,
          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<Env>;

API Reference

SafeDurableObjectBuilder(BaseClass, routerBuilder)

Creates a new Durable Object class with safe RPC methods.

Parameters

  • BaseClass: Your base Durable Object class
  • routerBuilder: 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:

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 DurableObjectState
  • env: The environment bindings
  • input: 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

Examples

Basic Counter

import { z } from "zod/v4";
import { SafeDurableObjectBuilder } from "safe-durable-objects";
import { DurableObject } from "cloudflare:workers";

export class Counter extends SafeDurableObjectBuilder(
  class extends DurableObject<Env> {
    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 };
      }),
  })
) {}

Error Handling

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.

const result = await stub.hello("invalid input").catch((error) => {
  /*handle here*/
});

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © Iterate

Support

If you have any questions or need help, please open an issue on GitHub.