git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
255 lines 8.0 kB history raw
1# purescript-yoga-fetch-om
3Derive type-safe fetch clients from `purescript-yoga-http-api` route definitions.
5## Installation
7```bash
8spago install yoga-fetch-om
9```
11Optional query examples below use [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill):
13```bash
14spago install justifill
15```
17## Migrating from 0.7
19Version 0.8 exposes optional query parameters as explicit `Maybe` fields. Calls that passed plain values no longer compile:
21```purescript
22-- 0.7: offset omitted
23users <- api.listUsers { limit: 10 }
25-- 0.8: every optional field is explicit
26users <- api.listUsers { limit: Just 10, offset: Nothing }
27allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
28```
30Import `Maybe(..)` from `Data.Maybe` when constructing these records directly. Alternatively, [`justifill`](https://pursuit.purescript.org/packages/purescript-justifill) wraps supplied values in `Just` and fills omitted fields with `Nothing`:
32```purescript
33-- offset remains omitted at the call site
34users <- api.listUsers (justifill { limit: 10 })
35```
37Path parameters and request bodies keep their existing argument shapes.
39## Quick Start
41### Example 1: Simple GET and POST Requests
43```purescript
44import Yoga.Fetch.Om.Simple (get, post)
46type User = { id :: Int, name :: String, email :: String }
48main = do
49 -- Simple GET request
50 user <- get @User "https://api.example.com/users/42" {}
51 log user.name
53 -- Simple POST request with JSON body
54 newUser <- post @User "https://api.example.com/users" {} { name: "Alice", email: "alice@example.com" }
55 log $ "Created user with ID: " <> show newUser.id
56```
58### Example 2: Type-Safe API Client with Full CRUD
60```purescript
61import Justifill (justifill)
63type UserAPI =
64 { health ::
65 Route GET "health" {}
66 ( ok :: { body :: { status :: String } } )
67 , getUser ::
68 Route GET ("users" / "id" : Int) {}
69 ( ok :: { body :: User }
70 , notFound :: { body :: ErrorMessage }
71 )
72 , listUsers ::
73 Route GET ("users" :? { limit :: Int, offset :: Int }) {}
74 ( ok :: { body :: Array User } )
75 , createUser ::
76 Route POST "users" { body :: JSON CreateUserRequest }
77 ( created :: { body :: User }
78 , badRequest :: { body :: ErrorMessage }
79 )
80 , updateUser ::
81 Route PUT ("users" / "id" : Int) { body :: JSON UpdateUserRequest }
82 ( ok :: { body :: User }
83 , notFound :: { body :: ErrorMessage }
84 , badRequest :: { body :: ErrorMessage }
85 )
86 , deleteUser ::
87 Route DELETE ("users" / "id" : Int) {}
88 ( noContent :: { body :: {} }
89 , notFound :: { body :: ErrorMessage }
90 )
91 }
93api = client @UserAPI "https://api.example.com"
95-- Generated methods use ordinary function application.
96health <- api.health
97user <- api.createUser { name: "Alice", email: "alice@example.com" }
98 # handleErrors
99 { badRequest: \err -> do
100 log $ "Validation error: " <> err.error
101 throw err
102 }
104-- Path parameters and request bodies remain separate arguments.
105updated <- api.updateUser
106 { id: user.id }
107 { name: "Alice Updated", email: user.email }
108 # handleErrors
109 { notFound: \_ -> throw userNotFound
110 , badRequest: \err -> throw validationError
111 }
113-- Optional query parameters are explicit `Maybe` fields. Justifill can wrap
114-- supplied values in `Just` and fill omitted fields with `Nothing`.
115users <- api.listUsers (justifill { limit: 10 })
117-- The equivalent call without Justifill:
118allUsers <- api.listUsers { limit: Nothing, offset: Nothing }
119```
121## More Examples
123See the test files for complete, runnable examples with all imports:
125- **[`test/Simple.Spec.purs`](test/Simple.Spec.purs)** - Basic GET/POST/PUT/PATCH/DELETE requests
126- **[`test/Complete.Example.purs`](test/Complete.Example.purs)** - Full CRUD API with error handling
127- **[`test/BuildUrl.Spec.purs`](test/BuildUrl.Spec.purs)** - Path parameters and query strings
128- **[`test/Variant.Spec.purs`](test/Variant.Spec.purs)** - Response variant handling
129- **[`test/SplitParams.Spec.purs`](test/SplitParams.Spec.purs)** - Parameter extraction patterns
131## Features
133### ✅ Type-Safe Everything
135- **Path parameters**: `/users/:id` requires `{ id :: Int }`; names and values are percent-encoded
136- **Query parameters**: Type-safe, percent-encoded query strings with explicit `Maybe` fields for optional values
137- **Request bodies**: `JSON`, `PlainText`, and URL-encoded `FormData`
138- **Response bodies**: Parsed JSON, `PlainText` as `String`, and `Streaming a` as `Strom {} () a`
139- **Error handling**: Exhaustive pattern matching on variants
141### ✅ Single Source of Truth
143Define your API once, use it everywhere:
145```purescript
146-- Server (yoga-fastify-om)
147server = buildServer apiRoutes handlers
149-- Client (yoga-fetch-om)
150apiClient = client @apiRoutes baseUrl
151```
153Changes to routes automatically update both client and server!
155### ✅ Automatic Derivation
157No manual client code:
158- ✅ URL building with segment-safe path substitution
159- ✅ Query construction that preserves existing queries and fragments
160- ✅ JSON, plain-text, and URL-encoded request serialization
161- ✅ JSON and plain-text response decoding
162- ✅ Incremental UTF-8 and binary response streams
163- ✅ Status code → variant mapping
165### ✅ Integration with yoga-om
167Works seamlessly with the Om monad:
169```purescript
170getUserProfile :: Om AppContext AppErrors User
171getUserProfile = do
172 { api, userId } <- ask
173 api.getUser { id: userId }
174 # handleErrors { notFound: \_ -> throw { userNotFound: userId } }
175```
177## How It Works
179The library uses PureScript's type system to:
1811. **Extract parameters** from route definitions at compile time
182 - Path params: `"users" / "id" : Int` → `{ id :: Int }`
183 - Optional query params: `:? { limit :: Int }` → `{ limit :: Maybe Int }`
184 - Required query params: `:? { limit :: Required Int }` → `{ limit :: Int }`
185 - Body params: `{ body :: JSON User }` → `User`
1872. **Build URLs** automatically
188 - Pattern: `/users/:id` + params: `{ id: 42 }` → `/users/42`
189 - Query: `?limit=10&offset=20`
1913. **Make requests** with `js-fetch` and `js-promise-aff`
1934. **Parse responses** by mapping status codes to variant labels
194 - `200` → `"ok"`, `404` → `"notFound"`, etc.
196All automatically based on your route types!
198## API Reference
200### `client`
202```purescript
203client :: forall @routes. String -> Record clients
204```
206Derives a record of client functions from a record of routes using Visible Type Application.
208**Parameters:**
209- `@routes` - Your API route type (provided via VTA syntax)
210- `baseUrl` - Base URL (e.g., `"https://api.example.com"`)
212**Returns:** Record where each route becomes a function returning `Om context errors result`
214**Example:**
215```purescript
216api = client @UserAPI "https://api.example.com"
217```
219## Features
221- ✅ Core client derivation with VTA syntax
222- ✅ All HTTP methods (GET, POST, PUT, PATCH, DELETE)
223- ✅ Path, query, and body parameters
224- ✅ Request/response headers
225- ✅ JSON encoding/decoding
226- ✅ FormData support
227- ✅ Variant response handling
228- ✅ Om monad integration
230## Testing
232```sh
233bun test
234bun run test:compile-fail
235bun run coverage
236```
237The suite combines example-based specifications with QuickCheck properties for
238URL encoding, suffix preservation, repeated parameters, optional fields, and
239query-separator invariants.
241The coverage command uses compiler source maps to report PureScript source
242coverage, includes JavaScript FFI modules, enforces regression thresholds, and
243writes HTML and LCOV reports to `coverage/`.
245## License
247MIT
249## Related Projects
251- [purescript-yoga-http-api](https://github.com/rowtype-yoga/purescript-yoga-http-api) - HTTP API type definitions
252- [purescript-yoga-fastify-om](https://github.com/rowtype-yoga/purescript-yoga-fastify-om) - Server-side counterpart
253- [purescript-yoga-json](https://github.com/rowtype-yoga/purescript-yoga-json) - JSON serialization
254- [purescript-yoga-om](https://github.com/rowtype-yoga/purescript-yoga-om) - Om monad