git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Add progressive examples to README
commit20c5f80faab0869e26def5c1a7b6f7baf01986e5 authorMark <mark.eibes@gmail.com> dateSun 8 Feb 2026, 15:33 (8 months ago) parent0d63100
1 file changed+236−78
README.md+236−78
README.md+236−78
@@ -2,97 +2,281 @@
22
33Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
44
5−## Concept
6−
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.
8−
95## Installation
106
117```bash
128spago install yoga-fetch-om
139```
1410
15−## Quick Start
11+## Progressive Examples
1612
17−### 1. Define your API using yoga-http-api
13+### Example 1: Simple GET Request
14+
15+The simplest possible client - just fetch a user by ID:
1816
1917```purescript
20−import Yoga.Fetch.Om
18+import Yoga.Fetch.Om (GET, Route, Path, type (/), type (:), deriveClient)
19+
20+type SimpleAPI =
21+ { getUser ::
22+ Route GET
23+ (Path ("users" / "id" : Int))
24+ {}
25+ ( ok :: { body :: User } )
26+ }
27+
28+api = deriveClient "https://api.example.com" (Proxy :: _ SimpleAPI)
29+
30+main = do
31+ user <- api.getUser { id: 42 }
32+ log user.name -- Type-safe! Compiler knows user is a User
33+```
34+
35+**Key concepts:**
36+- `Path ("users" / "id" : Int)` defines URL structure with path parameter
37+- `{ id: 42 }` becomes `/users/42`
38+- Response is automatically parsed to `User`
39+
40+### Example 2: Handling Multiple Response Types
2141
42+Real APIs return different responses for success vs errors:
43+
44+```purescript
2245type UserAPI =
2346 { getUser ::
2447 Route GET
2548 (Path ("users" / "id" : Int))
26− (Request {})
49+ {}
2750 ( ok :: { body :: User }
2851 , notFound :: { body :: ErrorMessage }
2952 )
30− , createUser ::
53+ }
54+
55+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
56+
57+main = do
58+ user <- api.getUser { id: 42 }
59+ # handleErrors
60+ { notFound: \err -> do
61+ log $ "User not found: " <> err.error
62+ pure defaultUser
63+ }
64+ log user.name
65+```
66+
67+**Key concepts:**
68+- Variant response types: `(ok :: ..., notFound :: ...)`
69+- `handleErrors` to convert error variants into values
70+- Type-safe error handling - can't forget a case!
71+
72+### Example 3: Query Parameters
73+
74+Add pagination with query strings:
75+
76+```purescript
77+type UserAPI =
78+ { listUsers ::
79+ Route GET
80+ (Path "users" :? { limit :: Int, offset :: Int })
81+ {}
82+ ( ok :: { body :: Array User } )
83+ }
84+
85+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
86+
87+main = do
88+ users <- api.listUsers { limit: 10, offset: 20 }
89+ -- Calls /users?limit=10&offset=20
90+ log $ "Found " <> show (length users) <> " users"
91+```
92+
93+**Key concepts:**
94+- `:?` operator adds query parameters
95+- `{ limit: 10, offset: 20 }` becomes `?limit=10&offset=20`
96+- Optional parameters use `Maybe` types
97+
98+### Example 4: POST with JSON Body
99+
100+Creating resources with request bodies:
101+
102+```purescript
103+type UserAPI =
104+ { createUser ::
31105 Route POST
32106 (Path "users")
33− (Request { body :: JSON CreateUserRequest })
107+ { body :: JSON CreateUserRequest }
34108 ( created :: { body :: User }
35109 , badRequest :: { body :: ErrorMessage }
36110 )
37111 }
38−```
39112
40−### 2. Derive a type-safe client
113+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
41114
42−```purescript
43−api = deriveAPI "https://api.example.com" (Proxy :: _ UserAPI)
115+main = do
116+ user <- api.createUser
117+ { name: "Alice"
118+ , email: "alice@example.com"
119+ }
120+ # handleErrors
121+ { badRequest: \err -> do
122+ log $ "Validation error: " <> err.error
123+ throw err
124+ }
125+ log $ "Created user with ID: " <> show user.id
44126```
45127
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
132+
133+### Example 5: PUT/PATCH with Path Parameters + Body
134+
135+Updating resources combines path params and body:
47136
48137```purescript
138+type UserAPI =
139+ { updateUser ::
140+ Route PUT
141+ (Path ("users" / "id" : Int))
142+ { body :: JSON UpdateUserRequest }
143+ ( ok :: { body :: User }
144+ , notFound :: { body :: ErrorMessage }
145+ , badRequest :: { body :: ErrorMessage }
146+ )
147+ }
148+
149+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
150+
49151main = do
50− -- Compiler ensures you pass the right parameters
51− result <- api.getUser { id: 42 }
52−
53− -- Pattern match on all possible responses
54− result # Variant.match
55− { ok: \(Response { body: user }) ->
56− log $ "User: " <> user.name
57− , notFound: \_ ->
58− log "User not found"
152+ user <- api.updateUser { id: 42 }
153+ { name: "Alice Updated"
154+ , email: "alice.new@example.com"
59155 }
156+ # handleErrors
157+ { notFound: \_ -> do
158+ log "User doesn't exist"
159+ throw userNotFound
160+ , badRequest: \err -> do
161+ log $ "Invalid update: " <> err.error
162+ throw validationError
163+ }
164+ log $ "Updated: " <> user.name
60165```
61166
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
63171
64−### ✅ Type-Safe Parameters
172+### Example 6: Complete CRUD API
65173
66−The compiler ensures you provide exactly the right parameters:
174+Putting it all together - a full CRUD API:
67175
68176```purescript
69−-- Path parameters
70−api.getUser { id: 42 }
71−
72−-- Query parameters
73−api.listUsers { limit: 10, offset: 20 }
74−
75−-- Request body
76−api.createUser { body: { name: "Alice", email: "alice@example.com" } }
177+type UserAPI =
178+ { getUser ::
179+ Route GET (Path ("users" / "id" : Int)) {}
180+ ( ok :: { body :: User }
181+ , notFound :: { body :: ErrorMessage }
182+ )
183+ , listUsers ::
184+ Route GET (Path "users" :? { limit :: Int, offset :: Int }) {}
185+ ( ok :: { body :: Array User } )
186+ , createUser ::
187+ Route POST (Path "users") { body :: JSON CreateUserRequest }
188+ ( created :: { body :: User }
189+ , badRequest :: { body :: ErrorMessage }
190+ )
191+ , updateUser ::
192+ Route PUT (Path ("users" / "id" : Int)) { body :: JSON UpdateUserRequest }
193+ ( ok :: { body :: User }
194+ , notFound :: { body :: ErrorMessage }
195+ , badRequest :: { body :: ErrorMessage }
196+ )
197+ , deleteUser ::
198+ Route DELETE (Path ("users" / "id" : Int)) {}
199+ ( noContent :: { body :: {} }
200+ , notFound :: { body :: ErrorMessage }
201+ )
202+ }
77203
78−-- Mixed parameters
79−api.updateUser { id: 42, body: { name: "Alice Updated" } }
204+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
205+
206+-- Use it anywhere in your app!
207+userWorkflow = do
208+ -- Create
209+ user <- api.createUser { name: "Alice", email: "alice@example.com" }
210+
211+ -- Read
212+ user <- api.getUser { id: user.id }
213+
214+ -- Update
215+ user <- api.updateUser { id: user.id } { name: "Alice Updated", email: user.email }
216+
217+ -- Delete
218+ _ <- api.deleteUser { id: user.id }
219+
220+ log "Workflow complete!"
80221```
81222
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`)
83227
84−Variant types ensure you handle all possible responses:
228+### Example 7: Integration with Om Monad
85229
86−```purescript
87−result <- api.createUser { body: newUser }
230+Using with `yoga-om` for effects and dependency injection:
88231
89−result # Variant.match
90− { created: \(Response { body }) -> log $ "Created: " <> show body.id
91− , badRequest: \(Response { body }) -> log $ "Error: " <> body.error
92− -- Forgot a case? Compile error!
232+```purescript
233+type AppContext =
234+ { api :: UserAPI
235+ , userId :: Int
93236 }
237+
238+type AppErrors =
239+ ( userNotFound :: Int
240+ , unauthorized :: String
241+ )
242+
243+getUserProfile :: Om AppContext AppErrors User
244+getUserProfile = do
245+ { api, userId } <- ask
246+ api.getUser { id: userId }
247+ # handleErrors
248+ { notFound: \_ -> throw { userNotFound: userId }
249+ }
250+
251+updateProfile :: UpdateUserRequest -> Om AppContext AppErrors User
252+updateProfile updates = do
253+ { api, userId } <- ask
254+ api.updateUser { id: userId } updates
255+ # handleErrors
256+ { notFound: \_ -> throw { userNotFound: userId }
257+ , badRequest: \err -> throw { unauthorized: err.error }
258+ }
94259```
95260
261+**Key concepts:**
262+- API client lives in context (dependency injection)
263+- Error handling with typed variants
264+- Composable with other Om-based effects
265+
266+## Complete Example
267+
268+See [`test/Complete.Example.purs`](test/Complete.Example.purs) for a complete working example with all patterns.
269+
270+## Features
271+
272+### ✅ Type-Safe Everything
273+
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
279+
96280### ✅ Single Source of Truth
97281
98282Define your API once, use it everywhere:
@@ -102,12 +286,12 @@ Define your API once, use it everywhere:
102286server = buildServer apiRoutes handlers
103287
104288-- Client (yoga-fetch-om)
105−client = deriveAPI baseUrl (Proxy :: _ apiRoutes)
289+client = deriveClient baseUrl (Proxy :: _ apiRoutes)
106290```
107291
108292Changes to routes automatically update both client and server!
109293
110−### ✅ Automatic Everything
294+### ✅ Automatic Derivation
111295
112296No manual client code needed:
113297- ✅ URL building with path parameter substitution
@@ -119,26 +303,7 @@ No manual client code needed:
119303
120304### ✅ Integration with yoga-om
121305
122−Works seamlessly with the Om monad:
123−
124−```purescript
125−getUserProfile :: Om AppContext AppErrors User
126−getUserProfile = do
127− { api, userId } <- ask
128− result <- liftAff $ api.getUser { id: userId }
129− case result of
130− ok -> pure result.body
131− notFound -> throw { userNotFound: userId }
132−```
133−
134−## Complete Example
135−
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
140−- DELETE requests
141−- Complete error handling
306+Works seamlessly with the Om monad for effects and dependency injection.
142307
143308## How It Works
144309
@@ -168,10 +333,10 @@ All of this happens automatically based on your route types!
168333
169334## API Reference
170335
171−### `deriveAPI`
336+### `deriveClient`
172337
173338```purescript
174−deriveAPI :: String -> Proxy (Record routes) -> Record clients
339+deriveClient :: String -> Proxy (Record routes) -> Record clients
175340```
176341
177342Derives 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.
180345- `baseUrl` - The base URL of the API (e.g., `"https://api.example.com"`)
181346- `routes` - Proxy of your API route definitions
182347
183−**Returns:** A record where each route becomes a function taking params and returning `Aff (Variant response)`
184−
185−### `deriveClientFn`
186−
187−```purescript
188−deriveClientFn :: String -> Proxy (Route method segments request response) -> ClientFn params response
189−```
190−
191−Derives a single client function from a route. Usually you'll use `deriveAPI` instead, but this is useful for individual routes.
348+**Returns:** A record where each route becomes a function taking params and returning `Om context errors result`
192349
193350## Roadmap
194351
@@ -199,6 +356,7 @@ Derives a single client function from a route. Usually you'll use `deriveAPI` in
199356- ✅ JSON encoding/decoding
200357- ✅ Variant response handling
201358- ✅ CORS support
359+- ✅ Om monad integration
202360
203361### Future
204362- ⏳ Request/response headers support