servant-purescript-codegen-example
servant-purescript-codegen-example copied to clipboard
Generate your PureScript types and API client from a Haskell Servant backend.
Haskell Servant + PureScript React with Code Generation :heart:

A demo application showcasing a Haskell Servant server + a PureScript React (react-basic-hooks and react-halo) client with code generation.
Running /server/codegen.sh will generate the API types together with a client, visible in Types.purs and ServerAPI.purs.
To achieve this the project uses purescript-bridge and servant-purescript.
For the application logic this project uses effectful, but there's an mtl version on the mtl branch.
Suggested workflow
This setup enables an extremely productive workflow as it takes very little effort to change things while being confident your client to server communication works properly.
Add your types and endpoint
-- API/Types.hs
...
data User = User
{ id :: UserId,
info :: UserData
}
deriving (Generic)
deriving anyclass (ToJSON, FromJSON)
data UserData = UserData
{ email :: Email,
username :: Username,
created :: CreatedAt
}
deriving (Generic)
deriving anyclass (ToJSON, FromJSON)
... other types omitted for brevity
-- API/Definition.hs
type UsersApi =
"users" :> Get '[JSON] [User]
Add the types to myTypes in API/CodeGen.hs
-- API/CodeGen.hs
myTypes :: [SumType 'Haskell]
myTypes =
[
genericShow $ equal $ argonaut $ mkSumType @User,
genericShow $ equal $ argonaut $ mkSumType @UserData,
... other types omitted for brevity
]
Run /server/codegen.sh.
Your types will appear on the client side.
-- API/Types.purs
newtype User = User
{ id :: UserId
, info :: UserData
}
instance Show User where
show a = genericShow a
derive instance Eq User
instance EncodeJson User where
encodeJson = defer \_ -> E.encode $ unwrap >$< (E.record
{ id: E.value :: _ UserId
, info: E.value :: _ UserData
})
instance DecodeJson User where
decodeJson = defer \_ -> D.decode $ (User <$> D.record "User"
{ id: D.value :: _ UserId
, info: D.value :: _ UserData
})
derive instance Generic User _
derive instance Newtype User _
-- ServerAPI.purs
getUsers ::
forall m.
MonadAjax Api m =>
m (Either (AjaxError JsonDecodeError Json) (Array User))
getUsers =
request Api req
where
req = { method, uri, headers, content, encode, decode }
method = Left GET
uri = RelativeRef relativePart query Nothing
headers = catMaybes
[
]
content = Nothing
encode = E.encode encoder
decode = D.decode decoder
encoder = E.null
decoder = D.value
relativePart = RelativePartNoAuth $ Just
[ "users"
]
query = Nothing
Link the newly generated API operation to your monad stack
-- Capability/Users.purs
class Monad m <= MonadUsers m where
listUsers :: m (Either APIError (Array User))
-- AppM.purs
instance monadUsersAppM :: MonadUsers AppM where
listUsers = callApi ServerAPI.getUsers
You can now use listUsers in your application code without duplicating any types or write any custom deserialization logic!
Known issues/quirks
I haven't yet found any dealbreakers, and most of these issues can be fixed with a PR, but still.
Endpoints using NoContent
For some reason if you use NoContent instead of () on your Servant routes the API call will result in a deserialization error on the PureScript side.
Required QueryParams don't become required
If you're using any QueryParams with '[Required], the PureScript code generation will not pick it up and you'll still have them as optional in ServerAPI.purs.
Haskell newtypes with a named field will not deserialize properly
For example if you have something like:
newtype Username = Username { unUsername :: Text }
this will fail to deserialize. A usable workaround is to define unUsername separately like so:
newtype Username = Username Text
unUsername :: Username -> Text
unUsername = coerce
What's not shown (yet)
- Handling polymorphic types
Running it locally
server
In ./server (assuming you have stack installed)
./run.sh
client
In ./client (assuming you have yarn installed)
yarn install
yarn run start