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

@thallesp/nestjs-restate

v0.1.1

Published

Restate for NestJS

Readme

NestJS Restate Integration

A NestJS integration for Restate: write Restate Services, Virtual Objects and Workflows as plain NestJS providers, with decorators, dependency injection and typed cross-service calls.

This library only wraps the official @restatedev/restate-sdk. The SDK is a peer dependency, so you pick the SDK version and keep using its APIs (Context, ctx.run, serde, TerminalError, ...) directly.

Installation

# Using npm
npm install @thallesp/nestjs-restate @restatedev/restate-sdk @restatedev/restate-sdk-clients

# Using yarn
yarn add @thallesp/nestjs-restate @restatedev/restate-sdk @restatedev/restate-sdk-clients

# Using pnpm
pnpm add @thallesp/nestjs-restate @restatedev/restate-sdk @restatedev/restate-sdk-clients

# Using bun
bun add @thallesp/nestjs-restate @restatedev/restate-sdk @restatedev/restate-sdk-clients

Prerequisites

  • A NestJS 11 or 12 application
  • @restatedev/restate-sdk and @restatedev/restate-sdk-clients >= 1.8.0
  • A running Restate server

Basic Setup

1. Import RestateModule

import { Module } from "@nestjs/common";
import { RestateModule } from "@thallesp/nestjs-restate";
import { Greeter } from "./greeter";

@Module({
  imports: [
    RestateModule.forRoot({
      port: 9080, // where Restate reaches this app
      ingress: { url: "http://localhost:8080" }, // where this app reaches Restate
    }),
  ],
  providers: [Greeter],
})
export class AppModule {}

2. Write a service

import type { Context } from "@restatedev/restate-sdk";
import { Handler, Service } from "@thallesp/nestjs-restate";
import { GreetingsService } from "./greetings.service";

@Service()
export class Greeter {
  constructor(private readonly greetings: GreetingsService) {}

  @Handler()
  async greet(ctx: Context, name: string) {
    const greeting = await ctx.run("pick greeting", () => this.greetings.pick());
    return `${greeting} ${name}`;
  }
}

3. Register the deployment

Once the app is up, tell Restate where it lives:

restate deployments register http://localhost:9080

The endpoint is served on its own HTTP/2 port and does not touch your HTTP adapter, so it works the same with Express, Fastify or a bare NestFactory.createApplicationContext.

[!NOTE] Restate classes are discovered from every module in the application, so provide them wherever they fit in your module tree. They must be singletons: request or transient scope (directly or through a dependency) is rejected at startup.

[!IMPORTANT] The endpoint starts in onApplicationBootstrap and NestJS runs that hook for RestateModule before the modules that import it. Set up anything a handler needs in onModuleInit (which always runs first) rather than onApplicationBootstrap. On shutdown the order is reversed, so your providers are destroyed before the endpoint stops. Invocations that hit either window fail and Restate retries them, so no work is lost.

Decorators

@Service()

Registers the class as a Restate Service. Every @Handler() method becomes a handler.

@Service({ name: "greeter", description: "Says hello" })
export class Greeter {
  @Handler()
  async greet(ctx: Context, name: string) {
    return `Hello ${name}`;
  }
}

Options are the SDK's ServiceOptions plus:

| Option | Description | | --- | --- | | name | Name registered in Restate. Defaults to the class name. | | description | Shown in the Restate UI and CLI. | | metadata | Free-form metadata for discovery. |

@VirtualObject()

Registers the class as a Virtual Object. @Handler() methods are exclusive, @Shared() methods run concurrently and can only read state.

import type { ObjectContext, ObjectSharedContext } from "@restatedev/restate-sdk";

@VirtualObject()
export class Counter {
  @Handler()
  async add(ctx: ObjectContext, amount: number) {
    const count = ((await ctx.get<number>("count")) ?? 0) + amount;
    ctx.set("count", count);
    return count;
  }

  @Shared()
  async get(ctx: ObjectSharedContext) {
    return (await ctx.get<number>("count")) ?? 0;
  }
}

Options are the SDK's ObjectOptions plus name, description and metadata.

@Workflow()

Registers the class as a Workflow. The run handler is the workflow itself, every other @Handler() is a shared handler that can signal it.

import type { WorkflowContext, WorkflowSharedContext } from "@restatedev/restate-sdk";

@Workflow()
export class Signup {
  @Handler()
  async run(ctx: WorkflowContext, user: { email: string }) {
    const code = await ctx.promise<string>("verified");
    return `${user.email} verified with ${code}`;
  }

  @Handler()
  async verify(ctx: WorkflowSharedContext, code: string) {
    await ctx.promise<string>("verified").resolve(code);
  }
}

Options are the SDK's WorkflowOptions plus name, description and metadata.

@Handler() and @Shared()

Mark a method as a handler. The first parameter is the Restate context, the optional second parameter is the input. Methods without the decorator are not exposed.

Both accept the SDK's per-handler options. Passing input or output serdes also type-checks the method signature against them:

import { serde } from "@restatedev/restate-sdk";
import { z } from "zod";

const Greeting = z.object({ name: z.string() });

@Service()
export class Greeter {
  @Handler({ input: serde.schema(Greeting), description: "Says hello" })
  async greet(ctx: Context, greeting: z.infer<typeof Greeting>) {
    return `Hello ${greeting.name}`;
  }
}

Calling other services

ref() turns a decorated class into the typed definition the SDK clients expect. The definition kind is inferred from the handlers' context type, so ctx.serviceClient(ref(Counter)) is a type error.

import { ref } from "@thallesp/nestjs-restate";

@Service()
export class Checkout {
  @Handler()
  async place(ctx: Context, order: Order) {
    const total = await ctx.objectClient(ref(Cart), order.cartId).total();
    ctx.workflowSendClient(ref(Fulfillment), order.id).run(order);
    await ctx.serviceClient(ref(Greeter)).greet(order.customer);
    return total;
  }
}

Only handler-shaped methods ((ctx, input?) => Promise) show up on the client. Constructor dependencies, lifecycle hooks and helpers are hidden.

ref() works from the class type alone, so two things are not checked at compile time: a handler-shaped method without @Handler() still appears on the client (Restate returns 404 for it), and a @VirtualObject() or @Workflow() whose handlers only declare Context is inferred as a service. Type the context parameter as ObjectContext/ObjectSharedContext or WorkflowContext/WorkflowSharedContext to get the right definition kind.

Calling from outside Restate

RestateIngress is the SDK's ingress client, connected with the ingress module option. Inject it anywhere:

import { RestateIngress, ref } from "@thallesp/nestjs-restate";

@Controller("signup")
export class SignupController {
  constructor(private readonly restate: RestateIngress) {}

  @Post()
  async signup(@Body() body: { email: string }) {
    await this.restate.workflowClient(ref(Signup), body.email).workflowSubmit(body);
  }

  @Post(":email/verify")
  verify(@Param("email") email: string, @Body("code") code: string) {
    return this.restate.workflowClient(ref(Signup), email).verify(code);
  }
}

Module Options

RestateModule.forRoot({
  port: 9080,
  ingress: { url: "http://localhost:8080", headers: { authorization: "Bearer ..." } },
  identityKeys: ["publickeyv1_..."],
  defaultServiceOptions: { journalRetention: { days: 1 } },
  logger: nestLoggerTransport(new Logger("Restate")),
});

| Option | Description | | --- | --- | | port | Port the Restate endpoint listens on. Defaults to 9080. | | ingress | ConnectionOpts for RestateIngress. Defaults to { url: "http://localhost:8080" }. | | identityKeys | Restate request identity public keys, same as the SDK's identityKeys. | | defaultServiceOptions | Options applied to every service, overridable per class and handler. | | logger | SDK LoggerTransport. Defaults to nestLoggerTransport(), which writes through the NestJS Logger. | | journalValueCodecProvider | Same as the SDK option. | | isGlobal | Register the module globally. Defaults to true. |

forRootAsync is supported with useFactory, useClass and useExisting:

RestateModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    port: config.get("RESTATE_PORT"),
    ingress: { url: config.get("RESTATE_INGRESS_URL") },
  }),
});

RestateEndpoint

The provider that serves the endpoint is exported as RestateEndpoint:

| Member | Description | | --- | --- | | port | The bound port, or undefined before bootstrap. Useful with port: 0 in tests. | | getDefinitions() | The SDK definitions built from the discovered classes, for example to pass to RestateTestEnvironment. |

The server starts on onApplicationBootstrap and stops on onModuleDestroy, so app.close() and enableShutdownHooks() behave as expected. Shutdown waits up to 5 seconds for in-flight invocations, then closes their connections and lets Restate retry them.

Testing

Restate classes are regular providers. For unit tests, instantiate them and pass a fake context. For integration tests, the SDK's @restatedev/restate-sdk-testcontainers can serve endpoint.getDefinitions(), or start the Restate container yourself and register http://host.docker.internal:${endpoint.port} like this repository's tests/e2e.test.ts does.