git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om
on main help sign in
git clone ssh://git.iko.soy/rowtype-yoga/purescript-yoga-fetch-om

purescript-yoga-fetch-om

Derive type-safe fetch clients from purescript-yoga-http-api route definitions.

Installation

spago install yoga-fetch-om

Optional query examples below use justifill:

spago install justifill

Migrating from 0.7

Version 0.8 exposes optional query parameters as explicit Maybe fields. Calls that passed plain values no longer compile:

-- 0.7: offset omitted
users <- api.listUsers { limit: 10 }

-- 0.8: every optional field is explicit
users <- api.listUsers { limit: Just 10, offset: Nothing }
allUsers <- api.listUsers { limit: Nothing, offset: Nothing }

Import Maybe(..) from Data.Maybe when constructing these records directly. Alternatively, justifill wraps supplied values in Just and fills omitted fields with Nothing:

-- offset remains omitted at the call site
users <- api.listUsers (justifill { limit: 10 })

Path parameters and request bodies keep their existing argument shapes.

Quick Start

Example 1: Simple GET and POST Requests

import Yoga.Fetch.Om.Simple (get, post)

type User = { id :: Int, name :: String, email :: String }

main = do
  -- Simple GET request
  user <- get @User "https://api.example.com/users/42" {}
  log user.name
  
  -- Simple POST request with JSON body
  newUser <- post @User "https://api.example.com/users" {} { name: "Alice", email: "alice@example.com" }
  log $ "Created user with ID: " <> show newUser.id

Example 2: Type-Safe API Client with Full CRUD

import Justifill (justifill)

type UserAPI =
  { health ::
      Route GET "health" {}
        ( ok :: { body :: { status :: String } } )
  , getUser ::
      Route GET ("users" / "id" : Int) {}
        ( ok :: { body :: User }
        , notFound :: { body :: ErrorMessage }
        )
  , listUsers ::
      Route GET ("users" :? { limit :: Int, offset :: Int }) {}
        ( ok :: { body :: Array User } )
  , createUser ::
      Route POST "users" { body :: JSON CreateUserRequest }
        ( created :: { body :: User }
        , badRequest :: { body :: ErrorMessage }
        )
  , updateUser ::
      Route PUT ("users" / "id" : Int) { body :: JSON UpdateUserRequest }
        ( ok :: { body :: User }
        , notFound :: { body :: ErrorMessage }
        , badRequest :: { body :: ErrorMessage }
        )
  , deleteUser ::
      Route DELETE ("users" / "id" : Int) {}
        ( noContent :: { body :: {} }
        , notFound :: { body :: ErrorMessage }
        )
  }

api = client @UserAPI "https://api.example.com"

-- Generated methods use ordinary function application.
health <- api.health
user <- api.createUser { name: "Alice", email: "alice@example.com" }
  # handleErrors
      { badRequest: \err -> do
          log $ "Validation error: " <> err.error
          throw err
      }

-- Path parameters and request bodies remain separate arguments.
updated <- api.updateUser
  { id: user.id }
  { name: "Alice Updated", email: user.email }
  # handleErrors
      { notFound: \_ -> throw userNotFound
      , badRequest: \err -> throw validationError
      }

-- Optional query parameters are explicit `Maybe` fields. Justifill can wrap
-- supplied values in `Just` and fill omitted fields with `Nothing`.
users <- api.listUsers (justifill { limit: 10 })

-- The equivalent call without Justifill:
allUsers <- api.listUsers { limit: Nothing, offset: Nothing }

More Examples

See the test files for complete, runnable examples with all imports:

Features

✅ Type-Safe Everything

✅ Single Source of Truth

Define your API once, use it everywhere:

-- Server (yoga-fastify-om)
server = buildServer apiRoutes handlers

-- Client (yoga-fetch-om)
apiClient = client @apiRoutes baseUrl

Changes to routes automatically update both client and server!

✅ Automatic Derivation

No manual client code:

✅ Integration with yoga-om

Works seamlessly with the Om monad:

getUserProfile :: Om AppContext AppErrors User
getUserProfile = do
  { api, userId } <- ask
  api.getUser { id: userId }
    # handleErrors { notFound: \_ -> throw { userNotFound: userId } }

How It Works

The library uses PureScript's type system to:

  1. Extract parameters from route definitions at compile time

    • Path params: "users" / "id" : Int → { id :: Int }
    • Optional query params: :? { limit :: Int } → { limit :: Maybe Int }
    • Required query params: :? { limit :: Required Int } → { limit :: Int }
    • Body params: { body :: JSON User } → User
  2. Build URLs automatically

    • Pattern: /users/:id + params: { id: 42 } → /users/42
    • Query: ?limit=10&offset=20
  3. Make requests with js-fetch and js-promise-aff

  4. Parse responses by mapping status codes to variant labels

    • 200 → "ok", 404 → "notFound", etc.

All automatically based on your route types!

API Reference

client

client :: forall @routes. String -> Record clients

Derives a record of client functions from a record of routes using Visible Type Application.

Parameters:

Returns: Record where each route becomes a function returning Om context errors result

Example:

api = client @UserAPI "https://api.example.com"

Features

Testing

bun test
bun run test:compile-fail
bun run coverage

The suite combines example-based specifications with QuickCheck properties for URL encoding, suffix preservation, repeated parameters, optional fields, and query-separator invariants.

The coverage command uses compiler source maps to report PureScript source coverage, includes JavaScript FFI modules, enforces regression thresholds, and writes HTML and LCOV reports to coverage/.

License

MIT