Skip to content

RPC

The effect/unstable/rpc modules let you describe a set of remote procedures once, as schema-backed definitions, and then derive both a typed client and a typed server from that single description. The client method signatures, the server handler signatures, the wire encoding, and the error channel are all generated from the same RpcGroup — so the two ends can never drift out of sync, and a payload or success type change is a compile error rather than a runtime surprise.

import { Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"
// A single shared contract. The client and server both import this group,
// guaranteeing they agree on tags, payloads, results, and errors.
class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()(
"UserNotFound",
{ id: Schema.String }
) {}
export class UserRpcs extends RpcGroup.make(
Rpc.make("GetUser", {
payload: { id: Schema.String }, // what the client sends
success: Schema.Struct({ id: Schema.String, name: Schema.String }),
error: UserNotFound // typed, schema-encoded failure
})
) {}

An RPC definition is just data. Nothing is sent or handled until the group is interpreted: a client reads the schemas to encode requests and decode responses, and a server reads the same schemas to decode requests, run your handlers, and encode results. The transport (HTTP, websocket, socket, worker, or an in-memory test harness) is chosen separately and never affects the typed surface.

RPC is the right tool when you control both ends and want a function-call feel across a process boundary — a backend service consumed by your own frontend, a worker thread, or a child process. It gives you typed payloads, typed errors, streaming results, and middleware without writing any serialization or routing code by hand.

If instead you need a public, REST-shaped HTTP surface with explicit paths, methods, and status codes — for third-party consumers or OpenAPI — reach for the HTTP API modules. RPC and HTTP API share the same Schema and Effect foundations, so the mental model carries over.

Everything in this section imports from effect/unstable/rpc. A working RPC setup is built from four roles:

  • The contractRpc and RpcGroup. Rpc.make declares one procedure (tag, payload, success, error, optional streaming); RpcGroup.make collects procedures into a single typed group that both ends import. This is pure data.
  • The implementationRpcServer plus handlers. You attach a handler to every procedure in the group (group.toLayer / group.toHandlers), then RpcServer decodes incoming requests, runs the matching handler, and encodes the results back over the transport.
  • The callerRpcClient. RpcClient.make turns the same group into an object of callable methods; each method encodes its payload, sends a request, and decodes the response into an Effect (or a Stream for streaming RPCs).
  • The wireRpcSerialization plus a transport Protocol. Serialization (RpcSerialization.layerJson, layerNdjson, layerMsgPack) chooses how values are encoded as bytes; the protocol layer (HTTP, websocket, socket, worker, or RpcTest in memory) chooses how those bytes move between client and server. Neither affects the typed surface from the contract.

These four roles also underpin higher-level runtimes: Cluster entities, for example, are defined as RpcGroups and reuse the same handler/client machinery.

  • Defining RPCs — declare a procedure with Rpc.make, attach payload, success, and error schemas, model streaming results with RpcSchema.Stream, and compose procedures into an RpcGroup (add, merge, prefix, annotateRpcs).
  • Client and server — implement handlers with group.toLayer, run them with RpcServer, build a typed client with RpcClient, and test the whole thing in-memory with RpcTest.
  • Middleware — cross-cutting concerns with RpcMiddleware: authentication, logging, context propagation, and adding middleware error types to both the client and server signatures.
  • Transports and serialization — wire the contract to a real channel with RpcSerialization (JSON, NDJSON, MsgPack) and the protocol layers for HTTP, websocket, socket, stdio, and worker.
  • Advanced RPC — streaming back pressure, asQueue clients, defect handling, request headers, primary-key payloads, and the makeNoSerialization escape hatch for already-decoded channels.

RPC builds directly on concepts covered elsewhere: Schema for the wire contract, Error Management for typed failures, Services & Layers for wiring, Streaming for streaming RPCs, and the Platform / HTTP Client modules for transports. Cluster builds distributed entities on top of RPC groups.