git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
Add progressive examples to README
20c5f80Mark
README.md+236−78
@@ -2,97 +2,281 @@
2
2
3Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
3Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
4
4
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−
9## Installation
5## Installation
10
6
11```bash
7```bash
12spago install yoga-fetch-om
8spago install yoga-fetch-om
13```
9```
14
10
15−## Quick Start
11+## Progressive Examples
16
12
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:
18
16
19```purescript
17```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
21
41
42+Real APIs return different responses for success vs errors:
43+
44+```purescript
22type UserAPI =
45type UserAPI =
23 { getUser ::
46 { getUser ::
24 Route GET
47 Route GET
25 (Path ("users" / "id" : Int))
48 (Path ("users" / "id" : Int))
26− (Request {})
49+ {}
27 ( ok :: { body :: User }
50 ( ok :: { body :: User }
28 , notFound :: { body :: ErrorMessage }
51 , notFound :: { body :: ErrorMessage }
29 )
52 )
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 =
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 }
36 )
110 )
37 }
111 }
38−```
39
112
40−### 2. Derive a type-safe client
113+api = deriveClient "https://api.example.com" (Proxy :: _ UserAPI)
41
114
42−```purescript
115+main = do
43−api = deriveAPI "https://api.example.com" (Proxy :: _ UserAPI)
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
44```
126```
45
127
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:
47
136
48```purescript
137```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+
49main = do
151main = do
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
57− , notFound: \_ ->
58− log "User not found"
59 }
155 }
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
60```
165```
61
166
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
63
171
64−### ✅ Type-Safe Parameters
172+### Example 6: Complete CRUD API
65
173
66−The compiler ensures you provide exactly the right parameters:
174+Putting it all together - a full CRUD API:
67
175
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 }
182+ )
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 }
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+ }
77
203
78−-- Mixed parameters
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
79−api.updateUser { id: 42, body: { name: "Alice Updated" } }
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!"
80```
221```
81
222
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`)
83
227
84−Variant types ensure you handle all possible responses:
228+### Example 7: Integration with Om Monad
85
229
86−```purescript
230+Using with `yoga-om` for effects and dependency injection:
87−result <- api.createUser { body: newUser }
88
231
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
93 }
236 }
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+ }
94```
259```
95
260
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+
96### ✅ Single Source of Truth
280### ✅ Single Source of Truth
97
281
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
103
287
104-- Client (yoga-fetch-om)
288-- Client (yoga-fetch-om)
105−client = deriveAPI baseUrl (Proxy :: _ apiRoutes)
289+client = deriveClient baseUrl (Proxy :: _ apiRoutes)
106```
290```
107
291
108Changes to routes automatically update both client and server!
292Changes to routes automatically update both client and server!
109
293
110−### ✅ Automatic Everything
294+### ✅ Automatic Derivation
111
295
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:
119
303
120### ✅ Integration with yoga-om
304### ✅ Integration with yoga-om
121
305
122−Works seamlessly with the Om monad:
306+Works seamlessly with the Om monad for effects and dependency injection.
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
142
307
143## How It Works
308## How It Works
144
309
@@ -168,10 +333,10 @@ All of this happens automatically based on your route types!
168
333
169## API Reference
334## API Reference
170
335
171−### `deriveAPI`
336+### `deriveClient`
172
337
173```purescript
338```purescript
174−deriveAPI :: String -> Proxy (Record routes) -> Record clients
339+deriveClient :: String -> Proxy (Record routes) -> Record clients
175```
340```
176
341
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
182
347
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`
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.
192
349
193## Roadmap
350## Roadmap
194
351
@@ -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
202
360
203### Future
361### Future
204- ⏳ Request/response headers support
362- ⏳ Request/response headers support