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.
| Capability | HTTP client or server | gRPC client or server |
|---|
client.invoker.invoke() | Calls the HTTP invocation API | Calls Dapr’s unary InvokeService API |
server.invoker.listen() | Registers an HTTP route | Registers an AppCallback.OnInvoke handler |
| Request verbs | GET, POST, PUT, PATCH, DELETE | Same verbs, carried in the HTTP extension |
| Custom request headers | options.headers is forwarded | Not forwarded as invocation metadata |
| Query parameters from the SDK client | Append an encoded query string to methodName | No separate query-string option |
| Default response decoding | JSON when parseable; otherwise text | JSON required; text or empty responses cause a parse error |
| Custom application serializer | Supported | Supported |
Response status and headers through invoke() | Not exposed | Not exposed |
Streaming through invoke() | Not supported; responses are buffered | Not supported; unary request/response |
client.proxy.create() | Not supported | Not 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
| Guide | Contents |
|---|
| Calling services | HTTP and gRPC clients, verbs, headers, paths, query strings, namespaces, and non-Dapr endpoints. |
| Receiving invocations | A complete server, HTTP and gRPC startup commands, callback data, return values, and existing Express applications. |
| Configuration and usage | Client 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>;
| Argument | Meaning |
|---|
appId | Target Dapr app ID, not the caller’s app ID or host address. |
methodName | Target method or relative HTTP path, for example orders or orders/101. Use no leading slash. |
method | HttpMethod.GET, POST, PUT, PATCH, or DELETE. Supply it explicitly; the implementations default to GET, but the public TypeScript interface requires it. |
data | Optional payload. Objects and arrays use JSON by default. |
options | An 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.
| Field | HTTP callback | gRPC callback |
|---|
body | Normally JSON.stringify(req.body) after the HTTP body parser; undefined when no body was parsed | UTF-8 decoding of the request data bytes; "" when no data was sent |
metadata.contentType | Incoming content-type header | Invocation request’s content type |
query | The original URL, including path and query string | The HTTP extension’s query-string field |
headers | Incoming HTTP headers; names are normally lowercase | Not 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.
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.
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.
| Failure | Behavior |
|---|
| HTTP sidecar or application error response | Rejects 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 error | Rejects with the underlying error, which need not have a JSON message. |
| gRPC transport or runtime failure | Rejects with the transport error; ConnectRPC failures use ConnectError codes, not HTTP status numbers. |
| gRPC non-JSON or empty response without a custom serializer | Rejects with an Error whose JSON message contains error: "COULD_NOT_PARSE_RESULT" and error_msg. |
| Custom response deserialization failure | Rejects 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.