Type-safe openapi typescript client with Effect

Languages

typescript5.6.3

Libraries

effect3.10.8
nodejs22.8.6
GithubCode

You can generate a type safe client using openapi-fetch and effect.

The typescript client is generated from an openapi definition file (example inside schema.yaml). We use openapi-typescript to do this, you can see an example from package.json.

Each request is defined inside requests.ts using Request.TaggedClass:

  • path: path of the endpoint for the request
  • schema: the schema of the response (using @effect/schema)
  • Parameters of the request (path, query, body)

Inside Api.ts we define a function for each request:

  • Use a Request defined in requests.ts (GetUserByUsername in the example)
  • Use open api client created in Client.ts (request function)

config.ts contains the Config definition for baseUrl (environmental variables) that exports a Layer for the client.

main.ts is the entry point of the application:

  • Effect.request is used to call the request
  • new GetUserByUsername defines the request parameters
  • Api.getUserByUsername is the request resolver

Finally, we use Effect.provide to inject the required dependencies and run the program.

You can see and run the full example on stackblitz.com.

import { Effect, Layer, Logger, LogLevel, RequestResolver } from "effect";
import { OpenApiClient } from "./Client";
import { GetUserByUsername } from "./requests";

const make = Effect.map(OpenApiClient, ({ request }) => ({
  // 👇 All the parameters are type-safe, the schema definition must conform to the OpenAPI schema
  getUserByUsername: RequestResolver.fromEffect((path: GetUserByUsername) =>
    request((client) =>
      client.GET(GetUserByUsername.path, {
        params: { path },
      })
    )(GetUserByUsername.schema)
  ),
}));

export class Api extends Effect.Tag("Api")<
  Api,
  Effect.Effect.Success<typeof make>
>() {
  static readonly Live = Layer.effect(this, make);

  // 👇 Inject `Mock` layer for local development testing
  static readonly Mock = make.pipe(
    Effect.map((live): typeof live => ({
      ...live,

      // 👇 Mock request
      getUserByUsername: RequestResolver.fromEffect((params) =>
        Effect.gen(function* () {
          yield* Effect.logDebug(params);
          return { username: "", uuid: "" };
        }).pipe(Logger.withMinimumLogLevel(LogLevel.All))
      ),
    })),
    Layer.effect(this)
  );
}