Implementing Procedure Handlers

Avatar of Hemanta SundarayHemanta Sundaray

Create a new file at ecom/product/handlers.ts and add the following code:

ecom/product/handlers.ts
import { Effect } from "effect";
import { productRpcGroup } from "./procedures.js";
import { ProductsService } from "./service.js";
export const productHandlersLayer = productRpcGroup.toLayer(
Effect.gen(function* () {
const products = yield* ProductsService;
return productRpcGroup.of({
GetProducts: () => products.getProducts(),
});
}),
);

A handler is the actual piece of code that executes your business logic whenever a specific procedure is invoked.

In Effect RPC, handlers for a group are defined using the group’s .toLayer() method. You pass it an Effect that, after acquiring any services it needs (here, ProductsService), returns an object mapping each procedure tag to its handler implementation.

To build that object, we call productRpcGroup.of({...}). The .of() helper is a type-safety guard: TypeScript ensures that every procedure declared in the group has a matching handler entry with the correct signature. If you forget one, you’ll get a compile error.

Understanding the Handler Function

Each handler function receives two arguments:

  1. payload: The validated input that the procedure was invoked with. This is fully typed against the procedure’s payload schema. Since GetProducts does not declare a payload, the argument is void, so we ignore it and define our handler as () => ....
  2. options: An object containing per-request metadata:
    • headers: The headers the client sent with this request.
    • requestId: A unique identifier for this request.
    • client: A ServerClient that you can use to make nested RPC calls back to the originating client.
    • rpc: A reference to the RPC definition itself.

Our GetProducts handler doesn’t need either of these. You’ll see the payload argument in action in the next chapter, where we define a procedure called GetProductById that takes a product ID as input.

Implementing the Business Logic

Let’s create the ProductsService to handle the actual data fetching from the DummyJSON API.

Create a new file at ecom/product/service.ts and add the following code:

ecom/product/service.ts
import { Effect, Layer, Context, Config, ConfigProvider } from "effect";
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from "effect/unstable/http";
import { ProductsFetchError, ProductsInvalidResponseError } from "./errors.js";
import { productListSchema, type ProductList } from "./schemas.js";
export class ProductsService extends Context.Service<ProductsService>()(
"app/ProductsService",
{
make: Effect.gen(function* () {
const baseUrl = yield* Config.string("BASE_URL").pipe(
Config.nested("DUMMY_JSON"),
);
const client = (yield* HttpClient.HttpClient).pipe(
HttpClient.mapRequest(HttpClientRequest.prependUrl(baseUrl)),
HttpClient.filterStatusOk,
);
// ====================================
// Fetches the full list of products
// ====================================
function getProducts(): Effect.Effect<
ProductList,
ProductsFetchError | ProductsInvalidResponseError
> {
return client.get("/products").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(productListSchema)),
Effect.catchTag("SchemaError", (error) =>
Effect.fail(
new ProductsInvalidResponseError({ cause: error.message }),
),
),
Effect.catchTag("HttpClientError", (error) =>
Effect.fail(new ProductsFetchError({ cause: error.message })),
),
);
}
return { getProducts };
}),
},
) {
static Live = Layer.effect(this, this.make).pipe(
Layer.provide(FetchHttpClient.layer),
Layer.provide(ConfigProvider.layer(ConfigProvider.fromDotEnv())),
);
}

There’s a lot going on here. Let’s break it down.

Reading the base URL from the environment

Inside ProductsService, we first define a baseUrl using the following code:

const baseUrl =
yield * Config.string("BASE_URL").pipe(Config.nested("DUMMY_JSON"));

Config.string("BASE_URL") describes what config value to read. Config.nested("DUMMY_JSON") adds a prefix, so Effect looks up DUMMY_JSON_BASE_URL. But where does it read this value from?

Every Effect program comes with a built-in ConfigProvider that reads from process.env. Because in our case the environment variable will be inside a .env file (which we’ll create next), we replace the built-in provider with ConfigProvider.fromDotEnv(), which reads the .env file from disk and parses it. We install it using Config.layer(ConfigProvider.fromDotEnv()), which tells Effect to use this provider instead of the built-in one.

Now when the service runs yield* Config.string("BASE_URL").pipe(Config.nested("DUMMY_JSON")), Effect looks up DUMMY_JSON_BASE_URL in the .env file instead of process.env.

Fetching products from DummyJSON

Inside the getProducts() function, we use the pre-configured client to perform a GET request to the /products endpoint. The result is then piped into HttpClientResponse.schemaBodyJson(productListSchema), which automatically parses the JSON response and validates it against our schema. If the validation fails, we catch the SchemaError and wrap it in a ProductsInvalidResponseError. Similarly, if the request itself fails, we catch the HttpClientError and return a ProductsFetchError.

Creating the .env File

Add a .env file at the root of the project and add the following environment variable:

.env
DUMMY_JSON_BASE_URL=https://dummyjson.com

Sign in to save progress

Stay in the loop

Get notified when Effect RPC related content is published.