Skip to content

Instantly share code, notes, and snippets.

@slinkydeveloper
Last active May 6, 2026 10:59
Show Gist options
  • Select an option

  • Save slinkydeveloper/31eea07ddb63d443c007b3cb96149d88 to your computer and use it in GitHub Desktop.

Select an option

Save slinkydeveloper/31eea07ddb63d443c007b3cb96149d88 to your computer and use it in GitHub Desktop.

Standalone handler styles

Bare generator

greet: function*(name: string) {
  return `Hello, ${name}!`;
},

typed(serdeOpts, fn): explicit serde

greet: typed(
  { input: serde.proto<GreetReq>(), output: serde.proto<GreetRes>() },
  function*({ name }) { return { message: `Hello, ${name}!` }; }
),

schemas(schemaOpts, fn): Zod/TypeBox shorthand

Equivalent to typed({ input: serde.schema(A), output: serde.schema(B) }, fn).

greet: schemas(
  { input: GreetReqSchema, output: GreetResSchema },
  function*({ name }) { return { message: `Hello, ${name}!` }; }
),

Full example

restate.service / restate.object / restate.workflow return { iface, implementation }. Options are a top-level options block; handler names in options.handlers are type-checked against the handlers map.

const { iface: greeterIface, implementation: greeter } = restate.service({
  name: "greeter",
  handlers: {
    *greet(name: string) {
      return `Hello, ${name}!`;
    },
    *greetWithRetention(name: string) {
      return `Hello, ${name}!`;
    },
    greetTyped: typed(
      { input: serde.proto<GreetReq>(), output: serde.proto<GreetRes>() },
      function*({ name }) { return { message: `Hello, ${name}!` }; }
    ),
  },
  options: {
    ...serviceLevelOptions,
    handlers: {
      greetWithRetention: { idempotencyRetention: "5m" },
    },
  },
});

const { iface: counterIface, implementation: counter } = restate.object({
  name: "counter",
  handlers: {
    *increment(delta: number) {
      const cur = yield* restate.state.get("count");
      yield* restate.state.set("count", (cur ?? 0) + delta);
    },
    *count() {
      return (yield* restate.state.get("count")) ?? 0;
    },
  },
  options: {
    handlers: {
      count: { shared: true },
    },
  },
});

// Calling other services ? use the iface directly as a client handle
const { implementation: caller } = restate.service({
  name: "caller",
  handlers: {
    *run() {
      const greeter = client(greeterIface);
      const result = yield* greeter.greet("world");

      const counter = client(counterIface, "my-counter");
      yield* counter.increment(1);
      const n = yield* counter.count();

      return `${result}, count is ${n}`;
    },
  },
});

Splitting interface from implementation

When callers live in a separate package, define the interface independently with restate.interface.* and publish it without the implementation as a dependency. The interface carries only type information ? no generator functions, no runtime options.

// @my-org/greeter-iface
export const greeterIface = restate.interface.service("greeter", {
  greet: restate.interface.typed<string, string>(),
  greetProto: restate.interface.typed({ input: serde.proto<GreetReq>(), output: serde.proto<GreetRes>() }),
  greetSchema: restate.interface.schemas({ input: GreetReqSchema, output: GreetResSchema }),
});

export const counterIface = restate.interface.object("counter", {
  increment: restate.interface.typed<number, void>(),
  count: restate.interface.typed<void, number>(),
});

The implementation imports the interface and calls iface.implement(). Same handlers + options shape as the standalone path.

// @my-org/greeter-impl
import { greeterIface, counterIface } from "@my-org/greeter-iface";
import { implement } from "@restate/sdk";

const greeter = implement(greeterIface, {
  handlers: {
    *greet(name) { return `Hello, ${name}!`; },
    *greetProto({ name }) { return { message: `Hello, ${name}!` }; },
    *greetSchema({ name }) { return { message: `Hello, ${name}!` }; },
  },
  options: {
    handlers: {
      greet: { retryPolicy: { maxAttempts: 3 } },
    },
  },
});

const counter = implement(counterIface, {
  handlers: {
    *increment(delta) {
      const cur = yield* restate.state.get("count");
      yield* restate.state.set("count", (cur ?? 0) + delta);
    },
    *count() {
      return (yield* restate.state.get("count")) ?? 0;
    },
  },
  options: {
    handlers: {
      count: { shared: true },
    },
  },
});

Callers depend only on @my-org/greeter-iface and use it as a client handle directly ? identical to the standalone case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment