Package

purescript-yoga-fetch-om

Repository
rowtype-yoga/purescript-yoga-fetch-om
License
MIT
Uploaded by
pacchettibotti
Published on
2026-09-12T16:33:18Z

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

  • Path parameters: /users/:id requires { id :: Int }; names and values are percent-encoded
  • Query parameters: Type-safe, percent-encoded query strings with explicit Maybe fields for optional values
  • Request bodies: JSON, PlainText, and URL-encoded FormData
  • Response bodies: Parsed JSON, PlainText as String, and Streaming a as Strom {} () a
  • Error handling: Exhaustive pattern matching on variants

✅ 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:

  • ✅ URL building with segment-safe path substitution
  • ✅ Query construction that preserves existing queries and fragments
  • ✅ JSON, plain-text, and URL-encoded request serialization
  • ✅ JSON and plain-text response decoding
  • ✅ Incremental UTF-8 and binary response streams
  • ✅ Status code → variant mapping

✅ 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:

  • @routes - Your API route type (provided via VTA syntax)
  • baseUrl - Base URL (e.g., "https://api.example.com")

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

Example:

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

Features

  • ✅ Core client derivation with VTA syntax
  • ✅ All HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • ✅ Path, query, and body parameters
  • ✅ Request/response headers
  • ✅ JSON encoding/decoding
  • ✅ FormData support
  • ✅ Variant response handling
  • ✅ Om monad integration

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

Related Projects