You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Production code needs more than `fetch()`: connection pooling, request timeouts, retries, logging, metrics, and proper resource management. Using `Effect.tryPromise` with raw `fetch` is fragile.
Problem
Production code needs more than fetch(): connection pooling, request timeouts, retries, logging, metrics, and proper resource management. Using Effect.tryPromise with raw fetch is fragile.
You need an HTTP client that:
Integrates natively with Effect
Handles connection pooling automatically
Supports timeouts and retries as first-class citizens
import{Effect,Schema,Duration,HttpClient,Layer}from"effect"import{HttpClientRequest,HttpClientResponse}from"@effect/platform"// 1. Define the schemaconstUser=Schema.Struct({id: Schema.Number,name: Schema.String,email: Schema.String,})typeUser=typeofUser.TypeconstparseUser=Schema.decodeUnknown(User)// 2. Create a typed HTTP client serviceinterfaceUserClient{readonlygetUser: (id: number)=>Effect.Effect<User>}constUserClient=Effect.Tag<UserClient>()// 3. Implement the serviceconstUserClientLive=Layer.succeed(UserClient,{getUser: (id: number)=>Effect.gen(function*(){constclient=yield*HttpClient.HttpClient// Build requestconstrequest=HttpClientRequest.get(`https://api.example.com/users/${id}`)// Execute request with timeoutconstresponse=yield*client(request).pipe(Effect.timeout(Duration.seconds(5)))// Validate statusconststatusCode=response.statusif(statusCode!==200){returnyield*Effect.fail(newError(`HTTP ${statusCode}: Failed to fetch user ${id}`))}// Parse body as JSONconstbody=yield*response.json// Validate against schemaconstuser=yield*parseUser(body)returnuser}).pipe(// Retry on transient errorsEffect.retry({times: 3,delay: Duration.millis(100),schedule: Effect.exponential(Duration.millis(100)),})),}satisfiesUserClient)// 4. Use the serviceconstfetchUser=(id: number)=>Effect.gen(function*(){constuserClient=yield*UserClientconstuser=yield*userClient.getUser(id)yield*Effect.log(`Fetched user: ${user.name} <${user.email}>`)returnuser})// 5. Run with proper resource managementconstmain=Effect.gen(function*(){constuser=yield*fetchUser(123)yield*Effect.log(`Processing user: ${user.email}`)})// Provide the HTTP client and UserClient layersconstlayer=Layer.merge(HttpClient.layer,// Built-in HTTP client from @effect/platformUserClientLive// Our user service)awaitEffect.runPromise(main.pipe(Effect.provide(layer)))
More Advanced: Custom Client with Middleware
import{Effect,Schema,Duration,HttpClient,Layer,Fiber}from"effect"import{HttpClientRequest}from"@effect/platform"// SchemasconstUser=Schema.Struct({id: Schema.Number,name: Schema.String,email: Schema.String,})typeUser=typeofUser.TypeconstparseUser=Schema.decodeUnknown(User)// Error typeclassApiErrorextendsError{constructor(readonlystatusCode: number,readonlypath: string,message: string){super(message)this.name="ApiError"}}// Custom HTTP client with logging and metricsinterfaceApiClient{readonlyget: <T>(path: string,schema: Schema.Schema<T>)=>Effect.Effect<T,ApiError>readonlybaseUrl: string}constApiClient=Effect.Tag<ApiClient>()constcreateApiClient=(baseUrl: string): Layer.Layer<ApiClient>=>Layer.succeed(ApiClient,{
baseUrl,get: (path,schema)=>Effect.gen(function*(){consthttpClient=yield*HttpClient.HttpClientyield*Effect.log(`GET ${path}`)constrequest=HttpClientRequest.get(`${baseUrl}${path}`)constresponse=yield*httpClient(request).pipe(Effect.timeout(Duration.seconds(10)),Effect.catchTag("TimeoutException",()=>Effect.fail(newApiError(0,path,`Request timeout for ${path}`))))// Check statusif(response.status!==200){consterrorBody=yield*response.textreturnyield*Effect.fail(newApiError(response.status,path,`HTTP ${response.status}: ${errorBody}`))}// Parse and validateconstbody=yield*response.jsonconstparsed=yield*Schema.decodeUnknown(schema)(body).pipe(Effect.mapError((error)=>newApiError(200,path,`Validation error: ${String(error)}`)))yield*Effect.log(`✓ ${path}`)returnparsed}).pipe(Effect.retry({times: 2,delay: Duration.millis(200),schedule: Effect.exponential(Duration.millis(200)),}),Effect.tapError((error)=>Effect.log(`✗ ${path}: ${error.message}`))),}satisfiesApiClient)// Use the custom clientconstgetUserService=(userId: number)=>Effect.gen(function*(){constclient=yield*ApiClientconstuser=yield*client.get(`/users/${userId}`,User)returnuser})// Batch operation with the clientconstfetchMultipleUsers=(userIds: number[])=>Effect.gen(function*(){constclient=yield*ApiClientyield*Effect.log(`Fetching ${userIds.length} users...`)constusers=yield*Effect.forEach(userIds,(id)=>client.get(`/users/${id}`,User))yield*Effect.log(`✓ Fetched ${users.length} users`)returnusers})// Run with proper dependenciesconstprogram=Effect.gen(function*(){constusers=yield*fetchMultipleUsers([1,2,3])yield*Effect.log(`Users: ${users.map((u)=>u.name).join(", ")}`)})constlayer=Layer.merge(HttpClient.layer,createApiClient("https://jsonplaceholder.typicode.com"))awaitEffect.runPromise(program.pipe(Effect.provide(layer)))