Effect Rpc http client complete example

Languages

typescript5.6.3

Libraries

effect3.10.8
nodejs22.8.6
GithubCode

This snippet implements an RPC server and client using effect:

  • effect: Core library
  • @effect/rpc: RPC library
  • @effect/rpc-http: RPC router and resolver for HTTP
  • @effect/platform: HTTP client requests (web handler)

The implementation starts from schema.ts. Inside it, we define all the requests supported by the RPC server. Each request is a TaggedRequest from @effect/schema:

  • failure
  • success
  • payload
export class SignUpRequest extends Schema.TaggedRequest<SignUpRequest>()(
  "SignUpRequest",
  {
    failure: RequestError,
    success: Schema.Boolean,
    payload: {
      email: Schema.NonEmptyString,
      password: Schema.String,
    },
  }
) {}

server.ts creates an RpcRouter. Each call to Rpc.effect uses a TaggedRequest to implement each request.

const router = RpcRouter.make(
  Rpc.effect(SignUpRequest, (params) =>
    Effect.gen(function* () {
      // 👇 `params` contains the `TaggedRequest` payload
      yield* Effect.log(params.email, params.password);
      return true;
    })
  )
);

We then export the Router type and a RpcWebHandler (handler for standard web Request/Response).

Using HttpApp.toWebHandlerLayer we can provide a Layer for the services used inside the requests.

The example shows how to add a custom logger.

Using Router we can derive a RpcClient that performs type-safe HTTP requests to the RPC server endpoint.

The client is fully type-safe since it's derived from Router, with both success, failure and payload types extracted from TaggedRequest.

RpcWebHandler can be used in any API supporting the standard Request/Response interface.

The example in route.ts shows how to use the RpcWebHandler in a NextJs API route.

import {
  FetchHttpClient,
  HttpClient,
  HttpClientRequest,
} from "@effect/platform";
import { RpcResolver } from "@effect/rpc";
import { HttpRpcResolverNoStream } from "@effect/rpc-http";
import { Effect, flow } from "effect";

// 👇 API derived from `Router` defined in the server
import { SignUpRequest } from "./schema";
import type { Router } from "./server";

export class RpcClient extends Effect.Service<RpcClient>()("RpcClient", {
  effect: Effect.gen(function* () {
    const baseClient = yield* HttpClient.HttpClient;
    const client = baseClient.pipe(
      HttpClient.mapRequest(
        // 👇 Rpc endpoint as `POST` pointing to a single API endpoint
        flow(
          HttpClientRequest.prependUrl("/api/rpc"),
          HttpClientRequest.setMethod("POST")
        )
      )
    );

    return HttpRpcResolverNoStream.make<Router>(client).pipe(
      RpcResolver.toClient
    );
  }),
  dependencies: [FetchHttpClient.layer],
}) {}

/// 👇 Example of how to use `RpcClient` to perform requests
const main = Effect.gen(function* () {
  const rpcClient = yield* RpcClient;

  //   👇 `boolean`
  const response = yield* rpcClient(
    new SignUpRequest({
      email: "test@test.com",
      password: "test",
    })
  ).pipe(
    Effect.tapError((requestError) => Effect.log(requestError.errorMessage))
  );
});