This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Service invocation with the JavaScript SDK

Call services and receive invocations over HTTP or gRPC with the JavaScript SDK

Use DaprClient.invoker.invoke() to call another application through your local Dapr sidecar, and DaprServer.invoker.listen() to receive invocations. The target can also be an existing HTTP application without the JavaScript SDK.

These guides describe @dapr/dapr 3.19.0 or later, including API and transport limitations.

Prerequisites

  • Dapr CLI and an initialized Dapr environment.
  • A supported Node.js LTS release. The package requires Node.js 18 or later.
  • A target application running with a Dapr app ID and application port. No state store or pub/sub component is required.

Install the 3.19.0 release:

npm install @dapr/dapr@3.19.0

For a later release, substitute its version. Examples use ES module imports; run snippets containing await inside an async function or an ES module.

Choose the protocol

The client’s communicationProtocol selects application-to-sidecar communication. The receiving application’s --app-protocol selects sidecar-to-application communication. These are independent: a gRPC client can invoke an HTTP application and an HTTP client can invoke a DaprServer configured for gRPC.

CapabilityHTTP client or servergRPC client or server
client.invoker.invoke()Calls the HTTP invocation APICalls Dapr’s unary InvokeService API
server.invoker.listen()Registers an HTTP routeRegisters an AppCallback.OnInvoke handler
Request verbsGET, POST, PUT, PATCH, DELETESame verbs, carried in the HTTP extension
Custom request headersoptions.headers is forwardedNot forwarded as invocation metadata
Query parameters from the SDK clientAppend an encoded query string to methodNameNo separate query-string option
Default response decodingJSON when parseable; otherwise textJSON required; text or empty responses cause a parse error
Custom application serializerSupportedSupported
Response status and headers through invoke()Not exposedNot exposed
Streaming through invoke()Not supported; responses are bufferedNot supported; unary request/response
client.proxy.create()Not supportedNot supported with the SDK’s ConnectRPC transport

Selecting gRPC does not turn invoke() into a client for arbitrary protobuf services. For native gRPC services, use your generated client with Dapr’s gRPC proxying protocol.

Guides

GuideContents
Calling servicesHTTP and gRPC clients, verbs, headers, paths, query strings, namespaces, and non-Dapr endpoints.
Receiving invocationsA complete server, HTTP and gRPC startup commands, callback data, return values, and existing Express applications.
Configuration and usageClient lifetime, endpoints, authentication, serialization, errors, resiliency, and API limitations.

Implementation references

1 - How to: Call services with the JavaScript SDK

Invoke application methods with HTTP or gRPC clients, headers, and query parameters

DaprClient.invoker.invoke() sends a request through the caller’s sidecar to a target application. These examples use @dapr/dapr 3.19.0 or later. Follow the receiving invocations guide to start the invocation-service target used below.

Invoke an application over HTTP

Save this complete example as client.mjs:

import { DaprClient, HttpMethod } from "@dapr/dapr";

async function main() {
  const client = new DaprClient({
    daprHost: "127.0.0.1",
    daprPort: "3501",
  });

  try {
    const response = await client.invoker.invoke(
      "invocation-service",
      "orders",
      HttpMethod.POST,
      { orderId: "101", quantity: 2 },
      { headers: { "X-Correlation-ID": "order-101" } },
    );
    console.log(response);
  } finally {
    await client.stop();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

In another terminal, run the caller with its own sidecar:

dapr run --app-id invocation-caller --dapr-http-port 3501 --dapr-grpc-port 50002 -- node client.mjs

The caller does not need --app-port because it does not receive callbacks. Its sidecar ports differ from those of the target service.

Invocation arguments

The public signature is:

invoke(
  appId: string,
  methodName: string,
  method: HttpMethod,
  data?: object,
  options?: InvokerOptions,
): Promise<object>;
ArgumentMeaning
appIdTarget Dapr app ID, not the caller’s app ID or host address.
methodNameTarget method or relative HTTP path, for example orders or orders/101. Use no leading slash.
methodHttpMethod.GET, POST, PUT, PATCH, or DELETE. Supply it explicitly; the implementations default to GET, but the public TypeScript interface requires it.
dataOptional payload. Objects and arrays use JSON by default.
optionsAn InvokerOptions object containing optional headers. It has no per-call timeout, cancellation, or retry fields.

The result is the decoded response body, not a response envelope. Although the public return type is object, HTTP may return text and a serializer may return other JavaScript values. There is no invoke<T>() overload and TypeScript types do not validate the response at runtime.

Select a verb

These snippets assume an existing client and matching routes on the target:

const order = await client.invoker.invoke("invocation-service", "orders/101", HttpMethod.GET);

const replaced = await client.invoker.invoke(
  "invocation-service", "orders/101", HttpMethod.PUT, { quantity: 4 },
);

const updated = await client.invoker.invoke(
  "invocation-service", "orders/101", HttpMethod.PATCH, { quantity: 3 },
);

await client.invoker.invoke("invocation-service", "orders/101", HttpMethod.DELETE);

Do not provide a body with an HTTP GET request. A gRPC invocation with omitted data sends the default payload {}, rather than an absent body.

Pass headers and query parameters over HTTP

Use the fifth argument for headers. To omit the body, pass undefined as the fourth argument:

const query = new URLSearchParams({ status: "pending", customer: "Ada Lovelace" });
const response = await client.invoker.invoke(
  "invocation-service",
  `orders?${query.toString()}`,
  HttpMethod.GET,
  undefined,
  { headers: { "X-Correlation-ID": "query-101" } },
);

Encode individual dynamic path segments with encodeURIComponent() and query values with URLSearchParams. The SDK interpolates methodName into the HTTP invocation URL; it does not encode it for you.

Custom headers are supported by the HTTP implementation. The gRPC implementation does not forward options.headers as request metadata. With a configured serializer, it only uses headers["Content-Type"] to override the payload content type. It also does not populate the request’s separate gRPC HTTP-extension query-string field, so do not use the HTTP query-string example with a gRPC client.

Invoke through a gRPC sidecar endpoint

Replace the client construction in client.mjs with:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50002",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

Keep the HttpMethod import and invoke() call from the original example, but omit the HTTP headers option. Use the same caller startup command: it exposes both sidecar endpoints. The receiving application can remain HTTP.

By default, the gRPC client decodes the response bytes as UTF-8 and parses them as JSON. An empty body or plain-text response rejects with COULD_NOT_PARSE_RESULT, even if the remote operation succeeded. Return a JSON value from the target, or configure a compatible serializer.

Call across namespaces

Where supported by Dapr’s name-resolution configuration, qualify the target app ID with its namespace:

const response = await client.invoker.invoke(
  "invocation-service.production", "orders", HttpMethod.POST, { orderId: "101" },
);

Namespace routing is a runtime capability, not a separate SDK option. See service invocation across namespaces.

Call a non-Dapr HTTP endpoint

With an HTTP client, the same API can target an HTTPEndpoint resource by name or a fully qualified URL, when supported by the runtime:

const configuredEndpoint = await client.invoker.invoke(
  "external-orders", "orders/101", HttpMethod.GET,
);

const urlEndpoint = await client.invoker.invoke(
  "https://api.example.com", "orders/101", HttpMethod.GET,
);

These are alternative examples: configure external-orders or replace the illustrative URL before running them. The target does not need a Dapr sidecar. See invoking non-Dapr endpoints for resource configuration, authentication, and runtime requirements.

2 - How to: Receive service invocations with the JavaScript SDK

Register HTTP or gRPC invocation handlers and understand callback requests and responses

Use DaprServer.invoker.listen(methodName, callback, options?) to register an invocation handler. These examples use @dapr/dapr 3.19.0 or later.

Start an HTTP service

Save this complete example as server.mjs:

import { DaprServer, HttpMethod } from "@dapr/dapr";

async function main() {
  const server = new DaprServer({
    serverHost: "127.0.0.1",
    serverPort: "3000",
    clientOptions: {
      daprHost: "127.0.0.1",
      daprPort: "3500",
    },
  });

  await server.invoker.listen(
    "orders",
    async (request) => {
      if (!request.body) {
        throw new Error("An order request body is required");
      }
      const order = JSON.parse(request.body);
      console.log("Received order:", order);
      console.log("Correlation ID:", request.headers?.["x-correlation-id"]);
      return { orderId: order.orderId, status: "accepted" };
    },
    { method: HttpMethod.POST },
  );

  await server.invoker.listen("health", async () => ({ healthy: true }));
  await server.start();

  async function stop() {
    await server.stop();
    await server.client.stop();
  }

  process.once("SIGINT", () => {
    stop().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  });
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run the service and its sidecar:

dapr run --app-id invocation-service --app-port 3000 --app-protocol http --dapr-http-port 3500 --dapr-grpc-port 50001 -- node server.mjs

Register handlers before server.start(). The listen() method defaults to GET; explicitly set { method: HttpMethod.POST } for the orders handler. The caller must use the same method name and verb. The health route is a regular example invocation route, not a configuration of Dapr’s application health checks.

Use the client example to call the service, or invoke it through the CLI:

dapr invoke --app-id invocation-service --method health --verb GET

Receive over gRPC

To use Dapr’s gRPC application callback API, add CommunicationProtocolEnum to the imports and replace the server construction with:

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "3000",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost: "127.0.0.1",
    daprPort: "50001",
  },
});

Keep the same handler registrations and startup code. Restart the target using:

dapr run --app-id invocation-service --app-port 3000 --app-protocol grpc --dapr-http-port 3500 --dapr-grpc-port 50001 -- node server.mjs

Unless overridden in clientOptions, the embedded client’s protocol follows the server protocol. serverPort is the application listener, while clientOptions.daprPort is the sidecar endpoint; do not interchange them. An HTTP caller can still invoke this gRPC callback service.

Understand the callback request

TypeScript handlers receive DaprInvokerCallbackContent, exported from @dapr/dapr. The callback is asynchronous and returns a promise for the response value.

FieldHTTP callbackgRPC callback
bodyNormally JSON.stringify(req.body) after the HTTP body parser; undefined when no body was parsedUTF-8 decoding of the request data bytes; "" when no data was sent
metadata.contentTypeIncoming content-type headerInvocation request’s content type
queryThe original URL, including path and query stringThe HTTP extension’s query-string field
headersIncoming HTTP headers; names are normally lowercaseNot populated

For a JSON request, explicitly parse request.body as in the example. Invocation handlers do not receive an automatically deserialized domain object, even when a custom serializer is configured.

For an HTTP callback, parse query parameters as a URL, not as a query string alone:

await server.invoker.listen("orders", async (request) => {
  const url = new URL(request.query ?? "/orders", "http://localhost");
  const status = url.searchParams.get("status");
  return { status };
}, { method: HttpMethod.GET });

For gRPC callbacks, query contains only the extension’s query string and can be read with URLSearchParams. The JavaScript gRPC invoker does not itself populate that field.

Return a response

Return the application response value directly:

await server.invoker.listen("version", async () => ({ version: "1.0" }));

By default, the SDK JSON-encodes truthy return values. There is no special { status, headers, body } response envelope: returning such an object sends those properties as application data. The callback API does not expose an HTTP response object or gRPC response metadata.

Without a custom serializer, undefined, null, false, 0, and "" do not produce their corresponding JSON values. An HTTP handler instead responds with {"closed":true}; a gRPC handler returns no data. A default gRPC client cannot parse that empty response. Prefer an object response such as { accepted: false }.

With a custom serializer, every return value except undefined is serialized, including falsy values. For undefined, HTTP still returns the closure object and gRPC still returns no data. See serialization guidance.

Thrown handler errors propagate to the underlying HTTP error middleware or gRPC transport; they are not successful application response objects. If callers need specific HTTP status codes, headers, or streaming responses, use a regular HTTP framework route instead.

Use an existing HTTP application

Dapr can invoke existing HTTP routes without DaprServer. Run that application with --app-port and --app-protocol http, and use its route path as methodName from a Dapr client.

To share an Express application with SDK callbacks, pass the Express instance as serverHttp:

import express from "express";
import { DaprServer } from "@dapr/dapr";

const app = express();
app.get("/orders/:id", (request, response) => {
  response.status(200).json({ orderId: request.params.id });
});

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "3000",
  serverHttp: app,
  clientOptions: { daprHost: "127.0.0.1", daprPort: "3500" },
});

await server.start();

Install express as an application dependency for this alternative example. Use the HTTP startup command above. server.start() starts the listener, so do not also call app.listen(). The SDK adds middleware and routes to the supplied app; avoid conflicting route names. Use your application’s shutdown handling to call server.stop() and then server.client.stop().

3 - Service invocation configuration and usage

Configure clients, serialization, authentication, and failure handling for JavaScript service invocation

This guidance applies to @dapr/dapr 3.19.0 or later. See calling services and receiving invocations for complete application examples.

Client lifetime

Reuse a long-lived DaprClient instead of creating one per invocation. Await outstanding work before calling await client.stop() during application shutdown. If using DaprServer, call await server.stop() to stop its listener, then await server.client.stop() to close its embedded client’s resources. Stopping the server alone does not stop that client.

The client initializes its connection and waits for the sidecar when first used. You can call await client.start() to check readiness before accepting application traffic. This connects to an existing sidecar; it does not launch the Dapr runtime process.

Endpoints and authentication

HTTP is the default protocol. Use the sidecar’s HTTP port for HTTP clients and its gRPC port for gRPC clients. Port options are strings:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

When neither host nor port is supplied, the SDK uses DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol if set. Otherwise it uses the configured host and port, falling back to 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT (defaults: 3500 and 50001). Supplying either host or port bypasses the endpoint environment variable.

daprApiToken defaults to DAPR_API_TOKEN. This authenticates the caller to its sidecar; it is not an application authorization header for the destination service. For HTTP destination authorization, supply the appropriate application header through InvokerOptions.headers. See Dapr API token authentication and service invocation access control.

Both transports propagate baggage from an active OpenTelemetry context to the sidecar. This built-in propagation does not mean the gRPC invoker supports arbitrary options.headers metadata.

Payload limits

The gRPC client uses maxBodySizeMb for its transport’s send and receive limits (default: 4 MiB). The HTTP client accepts the same option but does not currently enforce it; HTTP requests are limited by the sidecar and the target application. DaprServer.maxBodySizeMb controls the HTTP listener’s body-parser limit (4 MiB by default). These are separate settings; the client’s option is not an HTTP upload-size limit.

Configure the Dapr sidecar’s request body limit and the target application’s own limits as needed. Increasing a limit does not enable streaming or remove the invoker’s response buffering.

Serialization

Default behavior

Objects and arrays use JSON. The underlying serialization helpers also support text and binary request data, including Buffer, with inferred content types. The public invoke() TypeScript signature accepts data?: object, so primitive payloads are not part of that typed contract.

For HTTP, options.headers["Content-Type"] overrides the inferred request content type. Use that exact casing for the SDK’s serializer selection. Successful HTTP responses are read completely as text and parsed as JSON when possible, regardless of response content type; otherwise the text is returned.

For gRPC, the default request serializer infers the content type and does not use the headers option. Responses are decoded as UTF-8 and must contain JSON. Sending binary data does not make this a binary response API: neither invoker exposes response bytes or a response stream.

Configure an application serializer

Set serializer on DaprClient and clientOptions.serializer on DaprServer. IDaprSerializer is exported as a type from @dapr/dapr and defines:

interface IDaprSerializer {
  readonly contentType?: string;
  serialize<T>(value: T): string;
  deserialize<T = unknown>(data: string): T;
}

The SDK also exports JsonSerializer. For example, this opt-in JSON configuration ensures that falsy server return values other than undefined are serialized:

import { DaprClient, DaprServer, JsonSerializer } from "@dapr/dapr";

const serializer = new JsonSerializer();
const client = new DaprClient({ daprPort: "3501", serializer });
const server = new DaprServer({
  serverPort: "3000",
  clientOptions: { daprPort: "3500", serializer },
});

This configuration snippet does not start the server. For a custom encoding, implement IDaprSerializer and configure compatible serializers on both ends. Its wire representation is a string; binary formats must use a string-safe representation such as base64. Serialization errors reject the operation.

On invocation, the client serializes the request and deserializes the response. HTTP skips custom response deserialization for an empty response; gRPC passes even an empty response string to the deserializer.

Server invocation callbacks still receive a string body. Deserialize it explicitly:

import { HttpMethod } from "@dapr/dapr";

await server.invoker.listen("echo", async (request) => {
  // HTTP passes undefined for a missing body; gRPC passes an empty string.
  if (!request.body) {
    throw new Error("An invocation body is required");
  }
  return serializer.deserialize(request.body);
}, { method: HttpMethod.POST });

With a custom serializer, HTTP preserves non-JSON text or buffer request bodies as strings before invoking the handler. JSON bodies still pass through the HTTP JSON parser and are re-stringified. The server serializes the callback’s returned value using its configured serializer.

The serializer’s contentType defaults to application/json. Both clients can override it with headers["Content-Type"] when using a custom serializer, but the gRPC client still does not forward other custom headers. Changing the content type does not change the serializer implementation.

Errors and response information

Await invocations and handle failures at your application’s error boundary. Do not treat a rejected promise as proof that the target did not execute the operation.

FailureBehavior
HTTP sidecar or application error responseRejects with an Error whose message is a JSON string containing status, error (status text), and error_msg (response body). The HTTP helper accepts statuses 200 through 399; redirect handling follows its underlying HTTP client.
HTTP connection or serialization errorRejects with the underlying error, which need not have a JSON message.
gRPC transport or runtime failureRejects with the transport error; ConnectRPC failures use ConnectError codes, not HTTP status numbers.
gRPC non-JSON or empty response without a custom serializerRejects with an Error whose JSON message contains error: "COULD_NOT_PARSE_RESULT" and error_msg.
Custom response deserialization failureRejects with the serializer error.

invoke() returns only the decoded response body. It does not expose successful status codes, response headers, trailers, or structured per-call response metadata.

A missing gRPC callback registration currently logs a warning and returns an empty invocation response rather than a not-found application payload. The default gRPC client then fails to parse it. Check both the target’s logs and the caller’s error when diagnosing method or verb mismatches.

Timeouts, cancellation, and resiliency

InvokerOptions only contains headers. invoke() does not expose an AbortSignal, timeout, deadline, retry policy, or arbitrary gRPC call options. Do not copy timeout options from other SDKs or rely on illustrative API comments that show unsupported options.

Configure runtime timeouts, retries, and circuit breakers through Dapr resiliency policies. Policies apply in the runtime and do not add client-side cancellation to the JavaScript method. A local Promise.race() timeout only stops the caller waiting; it does not cancel the request or remote processing.

Design state-changing handlers to tolerate retries, for example by using an application request ID and deduplication. A timeout or response decoding failure can occur after the target has committed its work.

Native gRPC services and streaming

client.proxy.create() is not a working alternative in this version. HTTP rejects with HTTPNotSupportedError, and gRPC rejects because GRPCClientProxy is not supported with the ConnectRPC transport.

For a native protobuf service, create a generated gRPC client connected to the caller’s sidecar and attach dapr-app-id metadata as described in invoking services using gRPC. That is runtime gRPC proxying, separate from the SDK’s unary invoke() and DaprServer callback APIs.

For large HTTP requests or responses requiring streaming, use the runtime’s HTTP streaming support with an HTTP client and server that expose streams. The JavaScript invoker buffers the complete response and has no streaming overload.