1- // CHANGE: Example script demonstrating createClient API usage with automatic type inference
2- // WHY: Verify simplified API works as requested by reviewer without explicit type annotations
3- // QUOTE(TZ): "А почему он заставляет явно описать тип? apiClient.GET и так должен вернуть тип "
4- // REF: PR#3 comment from skulidropek
1+ // CHANGE: Example script demonstrating Effect-native error handling with createClient
2+ // WHY: Show how to handle HTTP errors (404, 500) via Effect error channel
3+ // QUOTE(TZ): "Мы не заставляем обрабатывать потенциальные исключения... Должно быть типо результат который принимается и потециальные исключения которые надо обработать "
4+ // REF: PR#3 comment from skulidropek about Effect representation
55// SOURCE: n/a
66// PURITY: SHELL
7- // EFFECT: Demonstrates Effect-based API calls with automatic type inference
7+ // EFFECT: Demonstrates Effect-based API calls with forced error handling
88
99import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
10- import { Console , Effect , Exit } from "effect"
10+ import { Console , Effect , Exit , Match } from "effect"
1111import { createClient , type ClientOptions } from "../src/shell/api-client/create-client.js"
1212import { dispatchercreatePet , dispatchergetPet , dispatcherlistPets } from "../src/generated/dispatch.js"
1313import type { Paths } from "../tests/fixtures/petstore.openapi.js"
14-
15- // Helper type for Error schema body
16- type ErrorBody = { readonly code : number ; readonly message : string }
17-
18- // Helper to check if body is an Error schema response
19- const isErrorBody = ( body : unknown ) : body is ErrorBody =>
20- typeof body === "object" &&
21- body !== null &&
22- "message" in body &&
23- typeof ( body as ErrorBody ) . message === "string"
14+ // Types are automatically inferred - no need to import them explicitly
2415
2516/**
2617 * Example: Create API client with simplified API
@@ -36,18 +27,21 @@ const clientOptions: ClientOptions = {
3627const apiClient = createClient < Paths > ( clientOptions )
3728
3829/**
39- * Example program: List all pets
30+ * Example program: List all pets with Effect-native error handling
31+ *
32+ * NEW DESIGN:
33+ * - Success channel (yield*): Only 2xx responses
34+ * - Error channel (catchTag/catchAll): HTTP errors (500) + boundary errors
4035 *
41- * NOTE: Types are now automatically inferred from the dispatcher!
42- * No explicit type annotation needed on the result variable.
36+ * This FORCES developers to handle HTTP errors explicitly!
4337 *
4438 * @pure false - performs HTTP request
4539 */
4640const listAllPetsExample = Effect . gen ( function * ( ) {
4741 yield * Console . log ( "=== Example 1: List all pets ===" )
4842
4943 // Execute request - type is automatically inferred from dispatcherlistPets
50- // No need for explicit type annotation!
44+ // Now: success = 200 only, error = 500 | BoundaryError
5145 const result = yield * apiClient . GET (
5246 "/pets" ,
5347 dispatcherlistPets ,
@@ -56,27 +50,36 @@ const listAllPetsExample = Effect.gen(function*() {
5650 }
5751 )
5852
59- // Pattern match on the response - TypeScript knows the possible statuses
60- if ( result . status === 200 ) {
61- const pets = result . body as Array < { id : string ; name : string ; tag ?: string } >
62- yield * Console . log ( `Success: Got ${ pets . length } pets` )
53+ // Success! We only get here if status was 200
54+ // No need to check status - TypeScript knows it's 200
55+ const pets = result . body
56+ yield * Console . log ( `Success: Got ${ pets . length } pets` )
57+ if ( pets . length > 0 ) {
6358 yield * Console . log ( ` First pet: ${ JSON . stringify ( pets [ 0 ] , null , 2 ) } ` )
64- } else if ( result . status === 500 && isErrorBody ( result . body ) ) {
65- yield * Console . log ( `Server error: ${ result . body . message } ` )
6659 }
67- } )
60+ } ) . pipe (
61+ // HTTP errors (500) now require explicit handling!
62+ Effect . catchTag ( "HttpError" , ( error ) =>
63+ Console . log ( `Server error (500): ${ JSON . stringify ( error . body ) } ` ) ) ,
64+ // Boundary errors are also in error channel
65+ Effect . catchTag ( "TransportError" , ( error ) =>
66+ Console . log ( `Transport error: ${ error . error . message } ` ) ) ,
67+ Effect . catchTag ( "UnexpectedStatus" , ( error ) =>
68+ Console . log ( `Unexpected status: ${ error . status } ` ) )
69+ )
6870
6971/**
7072 * Example program: Get specific pet
7173 *
72- * Demonstrates path parameters with automatic type inference .
74+ * Demonstrates handling multiple HTTP error statuses (404, 500) .
7375 *
7476 * @pure false - performs HTTP request
7577 */
7678const getPetExample = Effect . gen ( function * ( ) {
7779 yield * Console . log ( "\n=== Example 2: Get specific pet ===" )
7880
79- // Type is inferred from dispatchergetPet - no annotation needed!
81+ // Type is inferred from dispatchergetPet
82+ // Success = 200, Error = 404 | 500 | BoundaryError
8083 const result = yield * apiClient . GET (
8184 "/pets/{petId}" ,
8285 dispatchergetPet ,
@@ -85,21 +88,25 @@ const getPetExample = Effect.gen(function*() {
8588 }
8689 )
8790
88- if ( result . status === 200 ) {
89- const pet = result . body as { id : string ; name : string ; tag ?: string }
90- yield * Console . log ( `Success: Got pet "${ pet . name } "` )
91- yield * Console . log ( ` Tag: ${ pet . tag ?? "none" } ` )
92- } else if ( result . status === 404 && isErrorBody ( result . body ) ) {
93- yield * Console . log ( `Not found: ${ result . body . message } ` )
94- } else if ( result . status === 500 && isErrorBody ( result . body ) ) {
95- yield * Console . log ( `Server error: ${ result . body . message } ` )
96- }
97- } )
91+ // Success! Status is guaranteed to be 200
92+ yield * Console . log ( `Success: Got pet "${ result . body . name } "` )
93+ yield * Console . log ( ` Tag: ${ result . body . tag ?? "none" } ` )
94+ } ) . pipe (
95+ // Handle HTTP errors using Match for exhaustive pattern matching
96+ Effect . catchTag ( "HttpError" , ( error ) =>
97+ Match . value ( error . status ) . pipe (
98+ Match . when ( 404 , ( ) => Console . log ( `Not found: ${ JSON . stringify ( error . body ) } ` ) ) ,
99+ Match . when ( 500 , ( ) => Console . log ( `Server error: ${ JSON . stringify ( error . body ) } ` ) ) ,
100+ Match . orElse ( ( ) => Console . log ( `Unexpected HTTP error: ${ error . status } ` ) )
101+ ) ) ,
102+ Effect . catchTag ( "TransportError" , ( error ) =>
103+ Console . log ( `Transport error: ${ error . error . message } ` ) )
104+ )
98105
99106/**
100107 * Example program: Create new pet
101108 *
102- * Demonstrates POST requests with body .
109+ * Demonstrates handling validation errors (400) .
103110 *
104111 * @pure false - performs HTTP request
105112 */
@@ -111,7 +118,8 @@ const createPetExample = Effect.gen(function*() {
111118 tag : "cat"
112119 }
113120
114- // Type is inferred from dispatchercreatePet - no annotation needed!
121+ // Type is inferred from dispatchercreatePet
122+ // Success = 201, Error = 400 | 500 | BoundaryError
115123 const result = yield * apiClient . POST (
116124 "/pets" ,
117125 dispatchercreatePet ,
@@ -121,58 +129,55 @@ const createPetExample = Effect.gen(function*() {
121129 }
122130 )
123131
124- if ( result . status === 201 ) {
125- const pet = result . body as { id : string ; name : string ; tag ?: string }
126- yield * Console . log ( `Success: Created pet with ID ${ pet . id } ` )
127- yield * Console . log ( ` Name: ${ pet . name } ` )
128- } else if ( result . status === 400 && isErrorBody ( result . body ) ) {
129- yield * Console . log ( `Validation error: ${ result . body . message } ` )
130- } else if ( result . status === 500 && isErrorBody ( result . body ) ) {
131- yield * Console . log ( `Server error: ${ result . body . message } ` )
132- }
133- } )
132+ // Success! Status is guaranteed to be 201
133+ yield * Console . log ( `Success: Created pet with ID ${ result . body . id } ` )
134+ yield * Console . log ( ` Name: ${ result . body . name } ` )
135+ } ) . pipe (
136+ // Handle HTTP errors - FORCED by TypeScript!
137+ Effect . catchTag ( "HttpError" , ( error ) =>
138+ Match . value ( error . status ) . pipe (
139+ Match . when ( 400 , ( ) => Console . log ( `Validation error: ${ JSON . stringify ( error . body ) } ` ) ) ,
140+ Match . when ( 500 , ( ) => Console . log ( `Server error: ${ JSON . stringify ( error . body ) } ` ) ) ,
141+ Match . orElse ( ( ) => Console . log ( `Unexpected HTTP error: ${ error . status } ` ) )
142+ ) ) ,
143+ Effect . catchTag ( "TransportError" , ( error ) =>
144+ Console . log ( `Transport error: ${ error . error . message } ` ) )
145+ )
134146
135147/**
136- * Example program: Handle transport error
148+ * Example program: Using Effect.either for conditional error handling
137149 *
138- * Demonstrates error handling with Effect.either .
150+ * Demonstrates how to access both success and error in one place .
139151 *
140152 * @pure false - performs HTTP request
141153 */
142- const errorHandlingExample = Effect . gen ( function * ( ) {
143- yield * Console . log ( "\n=== Example 4: Error handling ===" )
144-
145- // Create client with invalid URL to trigger transport error
146- const invalidClient = createClient < Paths > ( {
147- baseUrl : "http://invalid.localhost:99999" ,
148- credentials : "include"
149- } )
154+ const eitherExample = Effect . gen ( function * ( ) {
155+ yield * Console . log ( "\n=== Example 4: Using Effect.either ===" )
150156
151157 const result = yield * Effect . either (
152- invalidClient . GET ( "/pets" , dispatcherlistPets )
158+ apiClient . GET ( "/pets/{petId}" , dispatchergetPet , {
159+ params : { petId : "999" } // Non-existent pet
160+ } )
153161 )
154162
155- if ( result . _tag === "Left" ) {
163+ if ( result . _tag === "Right" ) {
164+ // Success - got the pet
165+ yield * Console . log ( `Found pet: ${ result . right . body . name } ` )
166+ } else {
167+ // Error - check the type
156168 const error = result . left
157- if ( error . _tag === "TransportError" ) {
158- yield * Console . log ( `Transport error caught: ${ error . error . message } ` )
159- } else if ( error . _tag === "UnexpectedStatus" ) {
160- yield * Console . log ( `Unexpected status: ${ error . status } ` )
161- } else if ( error . _tag === "ParseError" ) {
162- yield * Console . log ( `Parse error: ${ error . error . message } ` )
163- } else {
164- yield * Console . log ( `Other error: ${ error . _tag } ` )
169+ if ( " _tag" in error ) {
170+ if ( error . _tag === "HttpError" ) {
171+ // HTTP error from schema (404 or 500)
172+ yield * Console . log ( `HTTP error ${ error . status } : ${ JSON . stringify ( error . body ) } ` )
173+ } else {
174+ // Boundary error (TransportError, UnexpectedStatus, etc. )
175+ yield * Console . log ( `Boundary error: ${ error . _tag } ` )
176+ }
165177 }
166- } else {
167- yield * Console . log ( "Expected error but got success" )
168178 }
169179} )
170180
171- /**
172- * Helper type for ApiFailure errors
173- */
174- type ApiError = { readonly _tag : string }
175-
176181/**
177182 * Main program - runs all examples
178183 *
@@ -181,38 +186,51 @@ type ApiError = { readonly _tag: string }
181186const mainProgram = Effect . gen ( function * ( ) {
182187 yield * Console . log ( "========================================" )
183188 yield * Console . log ( " OpenAPI Effect Client - Examples" )
189+ yield * Console . log ( " Effect-Native Error Handling" )
184190 yield * Console . log ( "========================================\n" )
185191
186- yield * Console . log ( "Demonstrating simplified API with automatic type inference:" )
187- yield * Console . log ( ' import createClient from "openapi-effect"' )
188- yield * Console . log ( " const client = createClient<Paths>({ ... })" )
189- yield * Console . log ( " const result = yield* client.GET(\"/path\", dispatcher)" )
190- yield * Console . log ( " // result is automatically typed!\n" )
192+ yield * Console . log ( "NEW DESIGN:" )
193+ yield * Console . log ( " - Success channel: 2xx responses only" )
194+ yield * Console . log ( " - Error channel: HTTP errors (4xx, 5xx) + boundary errors" )
195+ yield * Console . log ( " - Developers MUST handle HTTP errors explicitly!\n" )
196+
197+ yield * Console . log ( "Example code:" )
198+ yield * Console . log ( ' const result = yield* client.GET("/path", dispatcher)' )
199+ yield * Console . log ( " // result is 200 - no need to check status!" )
200+ yield * Console . log ( "" ) . pipe ( Effect . flatMap ( ( ) =>
201+ Console . log ( " // HTTP errors handled via Effect.catchTag or Effect.match\n" )
202+ ) )
191203
192204 // Note: These examples will fail with transport errors since
193205 // we're not connecting to a real server. This is intentional
194206 // to demonstrate error handling.
195207
196- yield * Effect . catchAll ( listAllPetsExample , ( error : ApiError ) =>
197- Console . log ( `Transport error (expected): ${ error . _tag === "TransportError" ? "Cannot connect to example server" : error . _tag } ` )
208+ yield * listAllPetsExample . pipe (
209+ Effect . catchAll ( ( error ) =>
210+ Console . log ( `Unhandled error in listAllPets: ${ JSON . stringify ( error ) } ` ) )
198211 )
199212
200- yield * Effect . catchAll ( getPetExample , ( error : ApiError ) =>
201- Console . log ( `Transport error (expected): ${ error . _tag === "TransportError" ? "Cannot connect to example server" : error . _tag } ` )
213+ yield * getPetExample . pipe (
214+ Effect . catchAll ( ( error ) =>
215+ Console . log ( `Unhandled error in getPet: ${ JSON . stringify ( error ) } ` ) )
202216 )
203217
204- yield * Effect . catchAll ( createPetExample , ( error : ApiError ) =>
205- Console . log ( `Transport error (expected): ${ error . _tag === "TransportError" ? "Cannot connect to example server" : error . _tag } ` )
218+ yield * createPetExample . pipe (
219+ Effect . catchAll ( ( error ) =>
220+ Console . log ( `Unhandled error in createPet: ${ JSON . stringify ( error ) } ` ) )
206221 )
207222
208- yield * errorHandlingExample
223+ yield * eitherExample . pipe (
224+ Effect . catchAll ( ( error ) =>
225+ Console . log ( `Unhandled error in either example: ${ JSON . stringify ( error ) } ` ) )
226+ )
209227
210228 yield * Console . log ( "\nAll examples completed!" )
211- yield * Console . log ( "\nType safety verification :" )
212- yield * Console . log ( " - Response types automatically inferred from dispatcher " )
213- yield * Console . log ( " - No explicit type annotations required " )
214- yield * Console . log ( " - All paths type-checked against OpenAPI schema " )
215- yield * Console . log ( " - All errors explicit in Effect type " )
229+ yield * Console . log ( "\nKey benefits of Effect-native error handling :" )
230+ yield * Console . log ( " - HTTP errors (404, 500) FORCE explicit handling " )
231+ yield * Console . log ( " - No accidental ignoring of error responses " )
232+ yield * Console . log ( " - Type-safe discrimination via _tag and status " )
233+ yield * Console . log ( " - Exhaustive pattern matching with Match.exhaustive " )
216234} )
217235
218236/**
0 commit comments