Type-safe openapi typescript client with Effect

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.

Languages

typescript

Libraries

effect
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)
  );
}