Schema validation for HTTP requests

When making a request with HttpClient we can also validate the response schema using Schema.

In a separate schema.ts file we define the schema for a post using Schema.Class:

src/services/schema.ts
import { Schema } from "effect";

export class Post extends Schema.Class<Post>("Post")({
  userId: Schema.Number,
  id: Schema.Number,
  title: Schema.String,
  body: Schema.String,
}) {}

The schema is based on the response from the jsonplaceholder API:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

Since getPosts returns an array of posts, it's convenient to define a schema for an array of posts directly inside the Post class:

export class Post extends Schema.Class<Post>("Post")({
  userId: Schema.Number,
  id: Schema.Number,
  title: Schema.String,
  body: Schema.String,
}) {
  static readonly Array = Schema.Array(this);
}

We then validate the response type using HttpClientResponse.schemaBodyJson:

  • Access the response body using Effect.flatMap
  • Apply the schema to validate the response with HttpClientResponse.schemaBodyJson
src/services/Api.ts
import {
  FetchHttpClient,
  HttpClient,
  HttpClientRequest,
  HttpClientResponse,
} from "@effect/platform";
import { Effect, flow } from "effect";
import { Post } from "./schema";

export class Api extends Effect.Service<Api>()("Api", {
  dependencies: [FetchHttpClient.layer],
  effect: Effect.gen(function* () {
    const baseClient = yield* HttpClient.HttpClient;
    const client = baseClient.pipe(
      HttpClient.mapRequest(
        flow(
          HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"),
          HttpClientRequest.acceptJson
        )
      )
    );

    return {
      getPosts: client
        .get("/posts")
        .pipe(
          Effect.flatMap(HttpClientResponse.schemaBodyJson(Post.Array)),
          Effect.scoped
        ),

      getPostById: (id: string) =>
        client
          .get(`/posts/${id}`)
          .pipe(
            Effect.flatMap(HttpClientResponse.schemaBodyJson(Post)),
            Effect.scoped
          ),
    };
  }),
}) {}

With this both the getPosts and getPostById methods are now type-safe:

  • getPosts returns Effect<readonly Post[], HttpClientError | ParseError>
  • getPostById returns Effect<Post, HttpClientError | ParseError>

ParseError is the error added by schemaBodyJson when the response doesn't match the schema.