@@ -2,97 +2,281 @@
3Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
3Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
7−Similar to how `purescript-yoga-fastify-om` derives servers from HTTP API definitions, this library derives **clients** from the same definitions using the Fetch API.
9## Installation
5## Installation
12spago install yoga-fetch-om
8spago install yoga-fetch-om
15−## Quick Start
11+## Progressive Examples
17−### 1. Define your API using yoga-http-api
13+### Example 1: Simple GET Request
15+The simplest possible client - just fetch a user by ID:
19```purescript
17```purescript
20−import Yoga.Fetch.Om
18+import Yoga.Fetch.Om (GET, Route, Path, type (/), type (:), deriveClient)
23+ (Path ("users" / "id" : Int))
25+ ( ok :: { body :: User } )
28+api = deriveClient "https://api.example.com" (Proxy :: _ SimpleAPI)
31+ user <- api.getUser { id: 42 }
32+ log user.name -- Type-safe! Compiler knows user is a User
36+- `Path ("users" / "id" : Int)` defines URL structure with path parameter
37+- `{ id: 42 }` becomes `/users/42`
38+- Response is automatically parsed to `User`
40+### Example 2: Handling Multiple Response Types
42+Real APIs return different responses for success vs errors:
22type UserAPI =
45type UserAPI =
23 { getUser ::
46 { getUser ::
25 (Path ("users" / "id" : Int))
48 (Path ("users" / "id" : Int))
27 ( ok :: { body :: User }
50 ( ok :: { body :: User }
28 , notFound :: { body :: ErrorMessage }
51 , notFound :: { body :: ErrorMessage }
55+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
58+ user <- api.getUser { id: 42 }
60+ { notFound: \err -> do
61+ log $ "User not found: " <> err.error
68+- Variant response types: `(ok :: ..., notFound :: ...)`
69+- `handleErrors` to convert error variants into values
70+- Type-safe error handling - can't forget a case!
72+### Example 3: Query Parameters
74+Add pagination with query strings:
80+ (Path "users" :? { limit :: Int, offset :: Int })
82+ ( ok :: { body :: Array User } )
85+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
88+ users <- api.listUsers { limit: 10, offset: 20 }
89+ -- Calls /users?limit=10&offset=20
90+ log $ "Found " <> show (length users) <> " users"
94+- `:?` operator adds query parameters
95+- `{ limit: 10, offset: 20 }` becomes `?limit=10&offset=20`
96+- Optional parameters use `Maybe` types
98+### Example 4: POST with JSON Body
100+Creating resources with request bodies:
30− , createUser ::
104+ { createUser ::
31 Route POST
105 Route POST
32 (Path "users")
106 (Path "users")
33− (Request { body :: JSON CreateUserRequest })
107+ { body :: JSON CreateUserRequest }
34 ( created :: { body :: User }
108 ( created :: { body :: User }
35 , badRequest :: { body :: ErrorMessage }
109 , badRequest :: { body :: ErrorMessage }
40−### 2. Derive a type-safe client
113+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
42−```purescript
115+main = do
43−api = deriveAPI "https://api.example.com" (Proxy :: _ UserAPI)
116+ user <- api.createUser
118+ , email: "alice@example.com"
121+ { badRequest: \err -> do
122+ log $ "Validation error: " <> err.error
125+ log $ "Created user with ID: " <> show user.id
46−### 3. Use it!
128+**Key concepts:**
129+- `{ body :: JSON CreateUserRequest }` defines request body
130+- Automatic JSON serialization with `yoga-json`
131+- Multiple response variants for different statuses
133+### Example 5: PUT/PATCH with Path Parameters + Body
135+Updating resources combines path params and body:
48```purescript
137```purescript
141+ (Path ("users" / "id" : Int))
142+ { body :: JSON UpdateUserRequest }
143+ ( ok :: { body :: User }
144+ , notFound :: { body :: ErrorMessage }
145+ , badRequest :: { body :: ErrorMessage }
149+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
50− -- Compiler ensures you pass the right parameters
152+ user <- api.updateUser { id: 42 }
51− result <- api.getUser { id: 42 }
153+ { name: "Alice Updated"
52−
154+ , email: "alice.new@example.com"
53− -- Pattern match on all possible responses
54− result # Variant.match
55− { ok: \(Response { body: user }) ->
56− log $ "User: " <> user.name
157+ { notFound: \_ -> do
158+ log "User doesn't exist"
160+ , badRequest: \err -> do
161+ log $ "Invalid update: " <> err.error
162+ throw validationError
164+ log $ "Updated: " <> user.name
62−## Features
167+**Key concepts:**
168+- Path params (`{ id: 42 }`) and body are separate parameters
169+- Multiple error variants can be handled independently
170+- Exhaustive pattern matching ensures all cases handled
64−### ✅ Type-Safe Parameters
172+### Example 6: Complete CRUD API
66−The compiler ensures you provide exactly the right parameters:
174+Putting it all together - a full CRUD API:
68```purescript
176```purescript
69−-- Path parameters
177+type UserAPI =
70−api.getUser { id: 42 }
178+ { getUser ::
71−
179+ Route GET (Path ("users" / "id" : Int)) {}
72−-- Query parameters
180+ ( ok :: { body :: User }
181+ , notFound :: { body :: ErrorMessage }
73−api.listUsers { limit: 10, offset: 20 }
183+ , listUsers ::
74−
184+ Route GET (Path "users" :? { limit :: Int, offset :: Int }) {}
75−-- Request body
185+ ( ok :: { body :: Array User } )
76−api.createUser { body: { name: "Alice", email: "alice@example.com" } }
186+ , createUser ::
187+ Route POST (Path "users") { body :: JSON CreateUserRequest }
188+ ( created :: { body :: User }
189+ , badRequest :: { body :: ErrorMessage }
192+ Route PUT (Path ("users" / "id" : Int)) { body :: JSON UpdateUserRequest }
193+ ( ok :: { body :: User }
194+ , notFound :: { body :: ErrorMessage }
195+ , badRequest :: { body :: ErrorMessage }
198+ Route DELETE (Path ("users" / "id" : Int)) {}
199+ ( noContent :: { body :: {} }
200+ , notFound :: { body :: ErrorMessage }
78−-- Mixed parameters
204+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
206+-- Use it anywhere in your app!
209+ user <- api.createUser { name: "Alice", email: "alice@example.com" }
212+ user <- api.getUser { id: user.id }
79−api.updateUser { id: 42, body: { name: "Alice Updated" } }
215+ user <- api.updateUser { id: user.id } { name: "Alice Updated", email: user.email }
218+ _ <- api.deleteUser { id: user.id }
220+ log "Workflow complete!"
82−### ✅ Exhaustive Response Handling
223+**Key concepts:**
224+- Single source of truth for your API
225+- All CRUD operations type-safe and derived automatically
226+- Share route definitions between client and server (with `yoga-fastify-om`)
84−Variant types ensure you handle all possible responses:
228+### Example 7: Integration with Om Monad
86−```purescript
230+Using with `yoga-om` for effects and dependency injection:
87−result <- api.createUser { body: newUser }
89−result # Variant.match
232+```purescript
90− { created: \(Response { body }) -> log $ "Created: " <> show body.id
233+type AppContext =
91− , badRequest: \(Response { body }) -> log $ "Error: " <> body.error
234+ { api :: UserAPI
92− -- Forgot a case? Compile error!
235+ , userId :: Int
239+ ( userNotFound :: Int
240+ , unauthorized :: String
243+getUserProfile :: Om AppContext AppErrors User
245+ { api, userId } <- ask
246+ api.getUser { id: userId }
248+ { notFound: \_ -> throw { userNotFound: userId }
251+updateProfile :: UpdateUserRequest -> Om AppContext AppErrors User
252+updateProfile updates = do
253+ { api, userId } <- ask
254+ api.updateUser { id: userId } updates
256+ { notFound: \_ -> throw { userNotFound: userId }
257+ , badRequest: \err -> throw { unauthorized: err.error }
262+- API client lives in context (dependency injection)
263+- Error handling with typed variants
264+- Composable with other Om-based effects
268+See [`test/Complete.Example.purs`](test/Complete.Example.purs) for a complete working example with all patterns.
272+### ✅ Type-Safe Everything
274+- **Path parameters**: Compiler ensures `/users/:id` gets an `id`
275+- **Query parameters**: Type-safe query strings
276+- **Request bodies**: Automatic JSON serialization
277+- **Response bodies**: Automatic JSON parsing
278+- **Error handling**: Exhaustive pattern matching on response variants
96### ✅ Single Source of Truth
280### ✅ Single Source of Truth
98Define your API once, use it everywhere:
282Define your API once, use it everywhere:
@@ -102,12 +286,12 @@ Define your API once, use it everywhere:
102server = buildServer apiRoutes handlers
286server = buildServer apiRoutes handlers
104-- Client (yoga-fetch-om)
288-- Client (yoga-fetch-om)
105−client = deriveAPI baseUrl (Proxy :: _ apiRoutes)
289+client = deriveClient baseUrl (Proxy :: _ apiRoutes)
108Changes to routes automatically update both client and server!
292Changes to routes automatically update both client and server!
110−### ✅ Automatic Everything
294+### ✅ Automatic Derivation
112No manual client code needed:
296No manual client code needed:
113- ✅ URL building with path parameter substitution
297- ✅ URL building with path parameter substitution
@@ -119,26 +303,7 @@ No manual client code needed:
120### ✅ Integration with yoga-om
304### ✅ Integration with yoga-om
122−Works seamlessly with the Om monad:
306+Works seamlessly with the Om monad for effects and dependency injection.
125−getUserProfile :: Om AppContext AppErrors User
127− { api, userId } <- ask
128− result <- liftAff $ api.getUser { id: userId }
130− ok -> pure result.body
131− notFound -> throw { userNotFound: userId }
136−See [`test/Complete.Example.purs`](test/Complete.Example.purs) for a full working example with:
137−- GET requests with path parameters
138−- GET requests with query parameters
139−- POST/PUT requests with JSON bodies
141−- Complete error handling
143## How It Works
308## How It Works
@@ -168,10 +333,10 @@ All of this happens automatically based on your route types!
169## API Reference
334## API Reference
171−### `deriveAPI`
336+### `deriveClient`
173```purescript
338```purescript
174−deriveAPI :: String -> Proxy (Record routes) -> Record clients
339+deriveClient :: String -> Proxy (Record routes) -> Record clients
177Derives a record of client functions from a record of routes.
342Derives a record of client functions from a record of routes.
@@ -180,15 +345,7 @@ Derives a record of client functions from a record of routes.
180- `baseUrl` - The base URL of the API (e.g., `"https://api.example.com"`)
345- `baseUrl` - The base URL of the API (e.g., `"https://api.example.com"`)
181- `routes` - Proxy of your API route definitions
346- `routes` - Proxy of your API route definitions
183−**Returns:** A record where each route becomes a function taking params and returning `Aff (Variant response)`
348+**Returns:** A record where each route becomes a function taking params and returning `Om context errors result`
188−deriveClientFn :: String -> Proxy (Route method segments request response) -> ClientFn params response
191−Derives a single client function from a route. Usually you'll use `deriveAPI` instead, but this is useful for individual routes.
193## Roadmap
350## Roadmap
@@ -199,6 +356,7 @@ Derives a single client function from a route. Usually you'll use `deriveAPI` in
199- ✅ JSON encoding/decoding
356- ✅ JSON encoding/decoding
200- ✅ Variant response handling
357- ✅ Variant response handling
201- ✅ CORS support
358- ✅ CORS support
359+- ✅ Om monad integration
203### Future
361### Future
204- ⏳ Request/response headers support
362- ⏳ Request/response headers support