In services/user-service.ts, add the following code:
import { Effect, Layer, ServiceMap } from "effect";import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse,} from "effect/unstable/http";
import { apiBaseUrlConfig } from "@/lib/config";import { USERS_PER_PAGE } from "@/lib/constants";import { mapHttpError } from "@/lib/http-error";import { ParseError, type HttpError } from "@/errors";import { UsersSchema, type UsersResponse } from "@/schema/user-schema";
export class UserService extends ServiceMap.Service<UserService>()( "app/UserService", { make: Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, );
const apiBaseUrl = yield* apiBaseUrlConfig;
// ============ Get Users ============ function getUsers(): Effect.Effect<UsersResponse, HttpError> { const request = HttpClientRequest.get(`${apiBaseUrl}/users`).pipe( HttpClientRequest.setUrlParams({ _limit: USERS_PER_PAGE.toString(), }), );
return client.execute(request).pipe( Effect.delay("1 second"), Effect.flatMap((response) => Effect.all({ users: HttpClientResponse.schemaBodyJson(UsersSchema)(response), usersCount: Effect.succeed( Number(response.headers["x-total-count"]), ), }), ), Effect.catchTag("HttpClientError", (error) => Effect.fail(mapHttpError(error)), ), Effect.catchTag("SchemaError", (error) => Effect.fail( new ParseError({ message: "Received an unexpected response from the server.", cause: error, }), ), ), ); }
return { getUsers }; }), },) { static layer = Layer.effect(this, this.make).pipe( Layer.provide(FetchHttpClient.layer), );}We define UserService using ServiceMap.Service. Let’s break down the key parts.
We obtain an HttpClient instance and pipe it through filterStatusOk. This ensures that any non-2xx response automatically fails the Effect with a HttpClientError with reason set to a StatusCodeError, so we don’t have to manually check status codes to distinguish between successful and failed responses.
The getUsers function constructs a GET request with the _limit query parameter and executes it. It returns an object with two properties: users and usersCount. The users property contains the response body, parsed and validated against UsersSchema. If the response doesn’t match the schema, the Effect fails with a SchemaError. The usersCount property contains the total number of users, read from the x-total-count response header.
Notice the Effect.delay("1 second") line. Since our
API reads from a local db.json file, responses are nearly instant. The
1-second delay simulates realistic network latency, giving us a chance to
observe loading states.
The _limit query parameter and the x-total-count response header are specific to json-server.
The _limit parameter controls how many items are returned per request. The x-total-count header contains the total number of users, which we’ll use to implement pagination later.
The UserService depends on HttpClient, which we provide via FetchHttpClient.layer in the static layer property.