Skip to content

razakiau/nestjs-restate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nest Logo

nestjs-restate

A native NestJS integration for Restate, the durable execution engine.

NPM Version CI Coverage License: MIT Ask DeepWiki

Overview

NestJS services don't survive crashes. If your app restarts mid-request, in-progress work is lost: partial payments, half-sent notifications, orphaned state.

Restate is a durable execution engine that fixes this. Every function call is persisted and retried from where it left off. No manual retry logic, idempotency keys, or state machines. nestjs-restate gives you Restate in NestJS with decorators, dependency injection, auto-discovery, and lifecycle management.

What you get:

  • Decorator-driven: @Service(), @VirtualObject(), @Workflow(), @Handler()
  • Full DI support: constructor injection works like any NestJS provider
  • NestJS execution pipeline: guards, interceptors, pipes, and exception filters work on Restate handlers
  • Auto-discovery: decorated classes are registered automatically, no manual wiring
  • Injectable context: RestateContext gives handler methods access to the Restate SDK context
  • Typed service proxies: call other Restate services with full type safety via @InjectClient(ServiceClass)
  • Typed Ingress client: call Restate services from REST controllers and cron jobs using decorated classes directly
  • Replay-aware logging: NestJS Logger calls are silenced during replay
  • Auto-registration: registers deployments with the Restate admin API on startup, with production hash checks and metadata change detection
  • SDK passthrough: retry policies, timeouts, and handler options forwarded directly to the Restate SDK

Installation

npm install nestjs-restate @restatedev/restate-sdk @restatedev/restate-sdk-clients

Peer Dependencies

Package Version
@nestjs/common ^10.0.0 || ^11.0.0
@nestjs/core ^10.0.0 || ^11.0.0
@restatedev/restate-sdk ^1.10.4
@restatedev/restate-sdk-clients ^1.10.4

Running Restate

You also need a running Restate server. The quickest way to get started locally:

# Docker
docker run --name restate -p 8080:8080 -p 9070:9070 docker.io/restatedev/restate:latest

# or Homebrew
brew install restatedev/tap/restate-server && restate-server

See the Restate deployment docs for Kubernetes, AWS Lambda, and other deployment options.

Quick Start

1. Import the module

import { Module } from '@nestjs/common';
import { RestateModule } from 'nestjs-restate';

@Module({
    imports: [
        RestateModule.forRoot({
            ingress: 'http://localhost:8080',
            endpoint: { port: 9080 },
        }),
    ],
})
export class AppModule {}

2. Define a service

import { Service, Handler, RestateContext } from 'nestjs-restate';

// stripe, mailer, db etc. in these examples are your own providers — not provided by this package

@Service('payments')
export class PaymentService {
    constructor(private readonly ctx: RestateContext) {}

    @Handler()
    async charge(input: { userId: string; amount: number }) {
        // ctx.run() makes this side effect durable:
        // if the service crashes after charging, Restate won't re-charge on retry
        const receipt = await this.ctx.run('charge-card', () =>
            stripe.charges.create({ amount: input.amount, customer: input.userId }),
        );

        await this.ctx.run('send-receipt', () =>
            mailer.send(input.userId, `Charged $${input.amount}. Receipt: ${receipt.id}`),
        );

        return { receiptId: receipt.id };
    }
}

How ctx.run() works: Restate journals the result of each ctx.run() call. On retry, it replays the journaled result instead of re-executing the function. This is what makes side effects like payments and emails safe without manual idempotency keys.

The Restate SDK context is not passed as a handler parameter. Instead, inject RestateContext via the constructor. It resolves to the correct context for each request using AsyncLocalStorage.

3. Register as a provider

@Module({
    providers: [PaymentService],
})
export class PaymentModule {}

Auto-discovery handles the rest. No manual registration with the Restate endpoint needed.

Concepts

Restate has three component types. Each is defined as a regular NestJS class with a decorator.

Services

Stateless durable handlers. If the service crashes mid-execution, Restate retries from the last checkpoint, not from the beginning. Use services for side effects like sending emails, charging payments, or calling external APIs.

import { Service, Handler, RestateContext } from 'nestjs-restate';

@Service('notifications')
export class NotificationService {
    constructor(
        private readonly ctx: RestateContext,
        private readonly mailer: MailProvider, // regular NestJS DI
    ) {}

    @Handler()
    async sendWelcome(input: { email: string; name: string }) {
        await this.ctx.run('send-email', () =>
            this.mailer.send(input.email, `Welcome, ${input.name}!`),
        );
    }
}

Virtual Objects

Stateful entities identified by a unique key. Each object instance gets its own key-value store managed by Restate. No external database needed. Exclusive handlers run one-at-a-time per key (consistency), while @Shared() handlers can run concurrently (reads).

import { VirtualObject, Handler, Shared, RestateContext } from 'nestjs-restate';

@VirtualObject('cart')
export class CartObject {
    constructor(private readonly ctx: RestateContext) {}

    @Handler() // exclusive — only one writer per cart key at a time
    async addItem(item: { sku: string; qty: number }) {
        const items = (await this.ctx.get<CartItem[]>('items')) ?? [];
        items.push(item);
        this.ctx.set('items', items);
        return items;
    }

    @Shared() // concurrent — safe for reads
    async getTotal() {
        const items = (await this.ctx.get<CartItem[]>('items')) ?? [];
        return items.reduce((sum, i) => sum + i.qty * i.price, 0);
    }
}

Use virtual objects for: shopping carts, user sessions, chat rooms, rate limiters, or any entity that needs consistent state without a separate database.

Workflows

Long-running durable processes with a unique execution per key. A workflow has one @Run() entry point and can receive external signals via @Signal() handlers while it runs.

import { Workflow, Run, Signal, Shared, RestateContext, TerminalError } from 'nestjs-restate';

@Workflow('user-signup')
export class SignupWorkflow {
    constructor(private readonly ctx: RestateContext) {}

    @Run()
    async run(input: { email: string }) {
        const code = this.ctx.rand.uuidv4().slice(0, 6);

        await this.ctx.run('send-code', () =>
            mailer.send(input.email, `Your code: ${code}`),
        );

        // Suspends until the user confirms — costs zero compute while waiting
        const submitted = await this.ctx.promise<string>('confirmation');

        if (submitted !== code) {
            throw new TerminalError('Invalid code', { errorCode: 400 });
        }

        await this.ctx.run('activate', () => db.activateUser(input.email));
        return { status: 'verified' };
    }

    @Signal()
    async confirm(code: string) {
        await this.ctx.promise<string>('confirmation').resolve(code);
    }

    @Shared()
    async status() {
        return this.ctx.promise<string>('confirmation')
            .peek()
            .then(() => 'confirmed')
            .catch(() => 'pending');
    }
}

Use workflows for: user onboarding, approval flows, order fulfillment, or any multi-step process that needs to wait for external events.

Key rules:

  • Exactly one @Run() per workflow. The method must be named run
  • @Signal() methods can be called concurrently while the workflow is running

@Signal() vs @Shared() on workflows: Both are concurrent handlers in the Restate SDK. Use @Signal() for methods that receive external input (resolving promises), and @Shared() for read-only queries (checking status). The distinction is semantic; they compile to the same handler type.

  • Use this.ctx.promise() for durable signals between run and signal handlers

Mental Model

If you know NestJS, you already know 80% of what you need:

You already know Restate equivalent What changes
@Injectable() service @Service() Handlers are durable. Side effects wrapped in ctx.run() survive crashes
Stateless service + database @VirtualObject() State lives in Restate's built-in key-value store, not your DB
Saga / multi-step job @Workflow() A durable process with signals, promises, and exactly-once completion
constructor(private svc: MyService) @InjectClient(MyService) Type-safe RPC between Restate services via DI
HTTP controller calling a service @InjectClient() Ingress Call Restate services from REST controllers, cron jobs, etc.
@UseGuards(), @UseInterceptors() Same decorators Guards, interceptors, pipes, and filters work on handlers automatically

Calling Services

From controllers and other NestJS code (Ingress)

Use the Ingress client to call Restate services from REST controllers, cron jobs, or any NestJS provider. Pass the decorated class directly:

import { Controller, Post, Body, Param } from '@nestjs/common';
import { InjectClient, type Ingress } from 'nestjs-restate';
import { PaymentService } from './payment.service';
import { CartObject } from './cart.object';

@Controller('api')
export class ApiController {
    constructor(@InjectClient() private readonly restate: Ingress) {}

    @Post('charge')
    async charge(@Body() body: { userId: string; amount: number }) {
        const client = this.restate.serviceClient(PaymentService);
        return client.charge(body);
    }

    @Post('cart/:key/add')
    async addToCart(@Param('key') key: string, @Body() item: CartItem) {
        const client = this.restate.objectClient(CartObject, key);
        return client.addItem(item);
    }
}

The Ingress type is re-exported from nestjs-restate.

If you need a raw SDK-compatible definition (e.g., for use with the SDK's own Ingress directly), use the definitionOf utilities:

import { serviceDefinitionOf, objectDefinitionOf, workflowDefinitionOf } from 'nestjs-restate';

serviceDefinitionOf(PaymentService);     // → { name: 'payments' }
objectDefinitionOf(CartObject);          // → { name: 'cart' }
workflowDefinitionOf(SignupWorkflow);    // → { name: 'user-signup' }

From handler to handler (typed proxies)

Inside a Restate handler, inject a typed proxy to call other services. These calls go through Restate, so they're durable, retried on failure, and journaled:

import { Service, Handler, InjectClient, RestateContext, type ServiceClient } from 'nestjs-restate';
import { PaymentService } from './payment.service';
import { NotificationService } from './notification.service';

@Service('orders')
export class OrderService {
    constructor(
        private readonly ctx: RestateContext,
        @InjectClient(PaymentService) private readonly payments: ServiceClient<PaymentService>,
        @InjectClient(NotificationService) private readonly notifications: ServiceClient<NotificationService>,
    ) {}

    @Handler()
    async place(input: { userId: string; amount: number }) {
        const receipt = await this.payments.charge(input);
        await this.notifications.sendWelcome({ email: input.userId, name: 'Customer' });
        return receipt;
    }
}

Typed proxies use AsyncLocalStorage and only work inside handler methods (@Handler(), @Run(), @Signal(), @Shared()).

For virtual objects, use ObjectClient<T>:

@InjectClient(CartObject) private readonly cart: ObjectClient<CartObject>

For workflows, use WorkflowClient<T>:

@InjectClient(SignupWorkflow) private readonly signup: WorkflowClient<SignupWorkflow>

Error Handling

Restate retries handler invocations when they fail. You need to know when to stop retries.

Terminal vs Retryable Errors

Error type Restate behavior
Regular Error Retried according to the retry policy (default: infinite)
TerminalError Not retried. Failure is written as output and returned to the caller
RetryableError Retried with an optional retryAfter delay hint
TimeoutError TerminalError subclass (code 408), returned by ctx.promise().orTimeout()
CancelledError TerminalError subclass (code 409), when an invocation is cancelled

Usage

Use TerminalError for non-retryable failures like validation errors, business rule violations, or permanent failures:

import { Service, Handler, RestateContext, TerminalError } from 'nestjs-restate';

@Service('orders')
export class OrderService {
    constructor(private readonly ctx: RestateContext) {}

    @Handler()
    async placeOrder(input: { userId: string; items: string[] }) {
        if (input.items.length === 0) {
            // Won't retry — this is a client error
            throw new TerminalError('Cart is empty', { errorCode: 400 });
        }

        // Regular errors (e.g., network failures) are retried automatically
        await this.ctx.run('charge-payment', () => paymentGateway.charge(input));
    }
}

Global Error Mapping

Use asTerminalError to automatically convert domain-specific errors into terminal errors:

RestateModule.forRoot({
    ingress: 'http://localhost:8080',
    endpoint: { port: 9080 },
    defaultServiceOptions: {
        asTerminalError: (error) => {
            if (error instanceof ValidationError) {
                return new TerminalError(error.message, { errorCode: 400 });
            }
            if (error instanceof NotFoundError) {
                return new TerminalError(error.message, { errorCode: 404 });
            }
            // Return undefined → Restate retries as normal
        },
    },
})

This also works per-component via decorator options:

@Service({
    name: 'payments',
    options: {
        asTerminalError: (error) => {
            if (error instanceof InsufficientFundsError) {
                return new TerminalError('Insufficient funds', { errorCode: 402 });
            }
        },
    },
})

All error classes (TerminalError, RetryableError, TimeoutError, CancelledError, RestateError) are re-exported from nestjs-restate.

Execution Pipeline

Guards, interceptors, pipes, and exception filters work on Restate handlers. Use @UseGuards(), @UseInterceptors(), @UseFilters() the same way you would on a controller.

Handler Parameter Decorators

Use @Input() and @Ctx() to inject handler arguments, same pattern as @Body() / @Param() for HTTP or @Args() for GraphQL:

@Service('payment')
export class PaymentService {
    constructor(private readonly gateway: PaymentGateway) {}

    @Handler()
    async charge(@Input() input: ChargeRequest) {
        return this.gateway.process(input);
    }

    @Handler()
    async refund(@Input('transactionId') txnId: string, @Ctx() ctx: Context) {
        await ctx.run('refund', () => this.gateway.refund(txnId));
    }
}
Decorator Description
@Input() Handler input (full object)
@Input('property') Single property from the input
@Ctx() Restate SDK context (Context, ObjectContext, WorkflowContext)

Handlers without decorators continue to work. @Input() is injected automatically as the first parameter when no decorators are present.

Guards and Interceptors

The handler args follow the RPC convention: context.switchToRpc().getData() returns the handler input, context.switchToRpc().getContext() returns the Restate SDK context. Use context.getType() to distinguish Restate from other context types:

@Injectable()
export class AmountLimitGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
        if (context.getType() !== 'restate') return true;
        const input = context.switchToRpc().getData();
        return input.amount <= 10_000;
    }
}

RestateExceptionFilter

Maps NestJS HTTP exceptions to Restate semantics. TerminalError passes through, 4xx becomes TerminalError (not retried), 5xx/unknown is rethrown (retried). Works alongside asTerminalError:

@Service('payment')
@UseFilters(RestateExceptionFilter)
export class PaymentService { ... }

To disable pipeline features globally, see pipeline in Configuration.

Logging

nestjs-restate ships a replay-aware logger that works out of the box.

How It Works

Restate replays handler invocations to rebuild state after crashes. During replay, log statements would produce duplicate, misleading output. The replay-aware logger solves this at two levels:

Direction What happens
NestJS → Restate Logger.overrideLogger() redirects all NestJS log calls. Inside a handler, logs are forwarded to ctx.console (replay-aware). Outside a handler, logs fall through to a standard ConsoleLogger.
Restate → NestJS A custom LoggerTransport is passed to createEndpointHandler(). SDK-internal messages are formatted with NestJS-style ANSI colors and written directly to stdout/stderr, and silenced during replay.

Usage

Use the standard NestJS Logger. It's replay-safe inside handlers with zero extra code:

import { Logger } from '@nestjs/common';
import { Service, Handler, RestateContext } from 'nestjs-restate';

@Service('greeter')
export class GreeterService {
    private readonly logger = new Logger(GreeterService.name);

    constructor(private readonly ctx: RestateContext) {}

    @Handler()
    async greet(name: string) {
        this.logger.log(`Greeting ${name}`);           // silenced during replay
        this.logger.debug(`Building greeting string`); // silenced during replay

        const greeting = await this.ctx.run('greeting', () => `Hello, ${name}!`);

        this.logger.log(`Greeting ready: ${greeting}`); // silenced during replay
        return greeting;
    }
}

You can also call this.ctx.console directly. Both approaches are replay-safe:

this.ctx.console.log('direct SDK logging');   // also silenced during replay

Level Mapping

NestJS level Restate ctx.console method
log info
error error
warn warn
debug debug
verbose trace
fatal error

Error Formatting

The logger transport automatically adjusts log levels to reduce noise and surface real problems:

Condition Original level Effective level
TerminalError at WARN WARN ERROR
RetryableError / plain Error at WARN WARN DEBUG
"Invocation suspended" at INFO INFO DEBUG

Recognized labels: [TerminalError], [RetryableError], [RestateError], [Error].

Exports

All logging primitives are re-exported from nestjs-restate if you need to customize:

import {
    RestateLoggerService,           // NestJS LoggerService implementation
    createRestateLoggerTransport,   // SDK LoggerTransport factory
    type LoggerTransport,           // SDK type
    type LoggerContext,             // SDK type
    type LogMetadata,               // SDK type
} from 'nestjs-restate';

Configuration

Module Options

RestateModule.forRoot({
    ingress: 'http://localhost:8080',          // Restate ingress URL (or { url, headers })
    endpoint: { port: 9080 },                 // HTTP/2 endpoint (see Endpoint Modes below)
    admin: 'http://localhost:9070',            // Admin API URL (or { url, authToken })
    autoRegister: {                           // Auto-register deployment on startup
        deploymentUrl: 'http://host.docker.internal:9080',
        force: true,                          // Overwrite existing (default: true)
    },
    identityKeys: [                           // Request identity verification keys
        'publickeyv1_...',
    ],
    defaultServiceOptions: {                  // Defaults applied to all components
        retryPolicy: {
            maxAttempts: 10,
            initialInterval: 100,
            exponentiationFactor: 2,
            maxInterval: 30_000,
        },
    },
    errors: {                                 // Error formatting in logs (see Logging)
        stackTraces: true,                    // Include stack traces (default: false)
    },
    pipeline: {                               // Execution pipeline (see Execution Pipeline)
        guards: true,                         // Enable guards (default: true)
        interceptors: true,                   // Enable interceptors (default: true)
        filters: true,                        // Enable exception filters (default: true)
    },
})

Async Configuration

import { ConfigService } from '@nestjs/config';

RestateModule.forRootAsync({
    inject: [ConfigService],
    useFactory: (config: ConfigService) => ({
        ingress: config.getOrThrow('RESTATE_INGRESS_URL'),
        admin: config.get('RESTATE_ADMIN_URL'),
        endpoint: {
            port: parseInt(config.getOrThrow('RESTATE_ENDPOINT_PORT'), 10),
        },
    }),
})

Endpoint Modes

endpoint: { port: 9080 }           // Standalone HTTP/2 server
endpoint: { server: myHttp2Server } // Attach to existing server
endpoint: { type: 'lambda' }       // AWS Lambda (no server)

Why a separate HTTP/2 server? Restate uses a binary protocol over HTTP/2 bidirectional streaming that can't be mounted as Express/Fastify middleware.

Restate Cloud

When using Restate Cloud, admin and ingress API calls require authentication:

RestateModule.forRoot({
    ingress: {
        url: process.env.RESTATE_INGRESS_URL,
        headers: { Authorization: `Bearer ${process.env.RESTATE_AUTH_TOKEN}` },
    },
    admin: {
        url: process.env.RESTATE_ADMIN_URL,
        authToken: process.env.RESTATE_AUTH_TOKEN,
    },
    endpoint: { port: 9080 },
    autoRegister: {
        deploymentUrl: process.env.RESTATE_DEPLOYMENT_URL,
    },
})
Option Purpose Affects
ingress.headers Custom headers for ingress client All service/object/workflow client calls
admin.authToken Bearer token for Restate admin API autoRegister deployment registration

Both ingress and admin also accept a plain URL string for non-authenticated setups.

To obtain your authentication token, log in via the Restate Cloud dashboard or run restate cloud login with the Restate CLI.

Component-Level Options

Decorators accept a string name, an options object, or nothing at all. Omitting the name defaults it to the class name:

@Service()                          // name → 'PaymentService'
export class PaymentService { ... }

For fine-grained SDK configuration, pass an options object:

@Service({
    name: 'payments',
    description: 'Payment processing service',
    metadata: { team: 'billing' },
    options: {
        retryPolicy: { maxAttempts: 5, initialInterval: 200 },
        inactivityTimeout: 30_000,
        ingressPrivate: true,
    },
})
export class PaymentService { ... }

@VirtualObject({
    name: 'cart',
    options: {
        enableLazyState: true,
        retryPolicy: { maxAttempts: 10 },
    },
})
export class CartObject { ... }

@Workflow({
    name: 'onboarding',
    options: {
        workflowRetention: 7 * 24 * 60 * 60 * 1000, // 7 days
        retryPolicy: { maxAttempts: 3 },
    },
})
export class OnboardingWorkflow { ... }

Handler-Level Options

Individual handlers can override component-level settings:

@Service('orders')
export class OrderService {
    constructor(private readonly ctx: RestateContext) {}

    @Handler({ retryPolicy: { maxAttempts: 1 } }) // no retries for idempotent ops
    async cancelOrder(orderId: string) { ... }

    @Handler({ inactivityTimeout: 60_000 }) // long timeout for slow operations
    async processReturn(input: ReturnRequest) { ... }
}

All SDK option types (RetryPolicy, ServiceOptions, ObjectOptions, WorkflowOptions, ServiceHandlerOpts, DefaultServiceOptions, etc.) are re-exported from nestjs-restate for convenience.

Auto-Registration

When autoRegister is set, the module calls the Restate admin API on startup to register the deployment.

Environment deploymentUrl
Docker Desktop http://host.docker.internal:9080
Local (no Docker) http://localhost:9080
Kubernetes http://my-service.default:9080

Use {{port}} for random port scenarios:

endpoint: { port: 0 },
autoRegister: { deploymentUrl: 'http://host.docker.internal:{{port}}' },

Registration Mode

By default, auto-registration uses force: true (development mode). Every restart overwrites the existing deployment. For production, use mode: 'production' to skip registration when the interface hasn't changed:

autoRegister: {
    deploymentUrl: 'http://my-service.default:9080',
    mode: 'production',        // GET pre-check + hash comparison
    metadata: { version: '2.1.0' },  // custom metadata sent with deployment
},
Mode Behavior
'development' (default) Always registers with force: true. Safe for local dev
'production' Computes a SHA-256 hash of the service interface. If the deployment already exists with the same hash, registration is skipped entirely. No unnecessary writes to Restate.

The hash is stored as nestjs-restate.interface-hash in the deployment metadata and can be inspected via the Restate admin API.

Deployment Change Detection

Restate uses immutable deployments: each code version gets a unique endpoint URL. New requests route to the latest deployment while in-flight invocations drain on the old one. This is the recommended approach for production (blue-green style), and platforms like AWS Lambda or the Restate Kubernetes operator handle it natively.

In practice, most NestJS applications deploy in-place: same URL, new code. There is no second deployment to drain to. When you register the updated endpoint with force: true, the old deployment is replaced immediately and any in-flight invocations tied to the previous code version may fail. This is where the onDeploymentMetadataChange hook comes in.

nestjs-restate diffs the metadata you tag on your components and calls your hook before registration happens. You get to inspect what changed and take action (cancel stale invocations, pause handlers, send alerts, or abort the registration entirely) before Restate replaces the old deployment.

Tag your components with metadata:

@Workflow({ name: 'order', metadata: { revision: '1' } })
export class OrderWorkflow { ... }

@Service({ name: 'payment', metadata: { version: '2.0' } })
export class PaymentService { ... }

Then configure the hook:

RestateModule.forRoot({
    admin: 'http://localhost:9070',
    endpoint: { port: 9080 },
    autoRegister: {
        deploymentUrl: 'http://host.docker.internal:9080',
        onDeploymentMetadataChange: async (changes, admin) => {
            for (const change of changes) {
                console.log(
                    `[deploy] ${change.serviceName} (${change.type}): ` +
                        `${JSON.stringify(change.oldMetadata)}${JSON.stringify(change.newMetadata)}`
                );
                // Use the admin connection to call the Restate admin API directly —
                // cancel invocations, pause handlers, or query state before the new
                // deployment replaces the old one.
                // See: https://docs.restate.dev/services/versioning#manual-versioning
            }
        },
    },
})

Each DeploymentMetadataChange describes one component:

interface DeploymentMetadataChange {
    serviceName: string;
    type: 'service' | 'virtualObject' | 'workflow' | 'unknown';
    oldMetadata: Record<string, string> | null;  // null = no metadata entry in current deployment
    newMetadata: Record<string, string> | null;  // null = no metadata entry in new version
}

The second argument gives you admin connection details ({ url: string; authToken?: string }) so your hook can call the Restate admin API directly.

Lifecycle:

  1. On startup, the module collects metadata from all decorated components
  2. It fetches existing deployments via GET /deployments and diffs old vs new metadata
  3. If anything changed, your hook is called before registration
  4. If the hook throws, registration is aborted. The old deployment stays active
  5. If the hook succeeds (or nothing changed), registration proceeds normally

The hook fires in both development and production modes. In production mode, the GET is shared with the hash pre-check, so there are no extra requests. If metadata changed but the interface hash has not, the module force-registers to persist the updated metadata.

If you use immutable deployments (unique URL per version), the hook is still useful for observability (you see what changed each deploy), but Restate already handles invocation draining for you. See Restate's versioning guide for the full model.

API Reference

Decorators

All class decorators implicitly apply @Injectable().

Decorator Description
@Service(name?) Restate Service. Stateless durable handlers. Name defaults to the class name when omitted.
@VirtualObject(name?) Restate Virtual Object. Keyed stateful handlers. Name defaults to the class name when omitted.
@Workflow(name?) Restate Workflow. Long-running durable process. Name defaults to the class name when omitted.
@Handler() Handler method on @Service, or exclusive handler on @VirtualObject
@Shared() Concurrent handler on @VirtualObject (for reads that can run in parallel)
@Signal() Signal handler on @Workflow (receives external signals while the workflow runs)
@Run() Entry point of a @Workflow (exactly one per workflow)
@InjectClient() Injects the enhanced Ingress client. Accepts decorated classes directly (for use outside handler context)
@InjectClient(ServiceClass) Injects a typed service proxy (handler context only, uses AsyncLocalStorage)
Injectable Description
RestateContext Injectable wrapper around the Restate SDK context, scoped to the current request via AsyncLocalStorage

Component and handler decorators also accept an optional options object for SDK-level configuration. See Configuration.

Context API

RestateContext exposes the full Restate SDK context surface. All methods delegate to the underlying SDK.

Category Method Description
Durable Execution run(action), run(name, action), run(name, action, options) Execute and persist side effects
Timers sleep(duration, name?) Durable sleep
Awakeables awakeable(serde?), resolveAwakeable(id, payload?, serde?), rejectAwakeable(id, reason) External event completion
State get(key, serde?), set(key, value, serde?), clear(key), clearAll(), stateKeys() Key-value store (objects/workflows)
Promises promise(name, serde?) Workflow durable promises
Invocations request(), cancel(invocationId), attach(invocationId, serde?) Invocation lifecycle
Generic Calls genericCall(call), genericSend(call) Untyped service invocation
Deterministic rand, date Seeded random & deterministic clock
Observability console Replay-aware logging
Identity key Object/workflow key
Escape Hatch raw Direct SDK context access

For service-to-service calls, use @InjectClient() with typed proxies instead of ctx.serviceClient(). See Calling Services.

Pipeline

Export Description
Input() Parameter decorator. Injects handler input (or a single property with @Input('prop'))
Ctx() Parameter decorator. Injects the Restate SDK context
RestateExceptionFilter Exception filter. 4xx HttpException becomes TerminalError, 5xx/unknown is rethrown
RestateExecutionContext Typed wrapper with getInput() / getRestateContext(), alternative to switchToRpc()
RestateContextType String literal 'restate', returned by context.getType()
PipelineOptions Configuration type for the pipeline module option

Migrating from v1

See MIGRATION.md for a complete migration guide with before/after examples.

Contributing

See CONTRIBUTING.md for development setup, commands, and guidelines.

License

MIT

About

First-class NestJS integration for Restate durable execution engine

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors