A client for Haskell programs to query a GraphQL API.
graphql-client
A client for Haskell applications to query GraphQL APIs. This package provides two resources:
A
graphql-codegen
executable that can generate Haskell definitions from input.graphql
filesThe
graphql-client
Haskell library providing arunQuery
function that takes in a query type generated bygraphql-codegen
Quickstart
Pre-requisites: Have Node.js installed.
Add
graphql-client
as a dependency to yourpackage.yaml
or Cabal filestack build --only-dependencies
Write the
.graphql
queries you wish to use.Write an appropriate
codegen.yml
configuration. It should look something like:schema: https://example.com/graphql documents: path/to/files/*.graphql hsSourceDir: src/ apiModule: Example.GraphQL.API enumsModule: Example.GraphQL.Enums scalarsModule: Example.GraphQL.Scalars
See the "Configuration" section for the full format of this file.
Write the module specified in
scalarsModule
(e.g.src/Example/GraphQL/Scalars.hs
). See the "Configuration" section for more details.stack exec graphql-codegen
The API module (e.g.
src/Example/GraphQL/API.hs
) should have been generated with the Haskell definitions needed to run your GraphQL queries. If any of your GraphQL queries use enums, corresponding modules will also be generated (see the "Configuration" section for more details).
The generated API creates a data type for each GraphQL query of the form {queryName}Query
(or {queryName}Mutation
for mutations). For example, the following GraphQL query would generate the following Haskell code:
query getRecordings($query: String!, $first: Int) {
search {
recordings(query: $query, first: $first) {
nodes {
title
}
}
}
}
data GetRecordingsQuery = GetRecordingsQuery
{ _query :: Text
, _first :: Maybe Int
}
type GetRecordingsSchema = [schema|
{
search: Maybe {
recordings: Maybe {
nodes: Maybe List Maybe {
title: Maybe Text,
},
},
},
}
|]
Data.GraphQL
exports a function runQuery
which takes in one of the Query or Mutation data types and returns the response, throwing an error if the GraphQL server returns an error.
A full example of the API in action:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
import Control.Monad.IO.Class (MonadIO(..))
import Data.GraphQL
( MonadGraphQLQuery
, GraphQLSettings(..)
, defaultGraphQLSettings
, get
, runGraphQLQueryT
, runQuery
)
import qualified Data.Text as Text
import Example.GraphQL.API
app :: (MonadGraphQLQuery m, MonadIO m) => m ()
app = do
song <- Text.pack <$> liftIO getLine
result <- runQuery GetRecordingsQuery
{ _query = song
, _first = Just 5
}
-- See the `aeson-schemas` package for more information on this syntax
let songs = [get| result.search!.recordings!.nodes![]! |]
liftIO $ print $ map [get| .title! |] songs
main :: IO ()
main = do
let graphQLSettings = defaultGraphQLSettings
{ url = "https://graphbrainz.fly.dev"
-- ^ Most GraphQL APIs are at the path `/graphql`, but not this one
}
runGraphQLQueryT graphQLSettings app
Configuration
The codegen.yml
file should have the following format. All paths are relative to the codegen.yml
file.
schema
: Where to get the schema of the entire GraphQL API. Can be one of the following:- A URL pointing to the GraphQL API
- The path to a local JSON file containing the result of a GraphQL Introspection query
- The path to a local
.graphql
file containing the schema in GraphQL format
documents
: A string or list of strings containing glob expressions to load the.graphql
files containing the GraphQL queries you wish to use.hsSourceDir
: The directory (relative tocodegen.yml
) to generate the modules. Should be one of the directories in thehs-source-dirs
field in your Cabal file. A moduleX.Y.Z
would be generated at<hsSourceDir>/X/Y/Z.hs
. Defaults tosrc/
.apiModule
: The module that will be generated with the Haskell definitions corresponding to the.graphql
input files specified bydocuments
.enumsModule
: The module where GraphQL enums will be generated. Only the enums you actually use in your queries will be generated, with a module generated per enum. For example, if your queries use aColor
enum andenumsModule
is set toExample.GraphQL.Enums
,graphql-codegen
will generate theExample.GraphQL.Enums.Color
module.scalarsModule
: The module where custom GraphQL scalars should be exported. You may define the scalars in other modules, but you must re-export them in this module. If you're not using any custom scalars in your queries, this module can be empty (but must still exist). All GraphQL scalars must haveFromJSON
andToJSON
instances.
Testing
This library also provides utilities to test functions using GraphQL queries by mocking the GraphQL endpoints. For example, you might test the app
function from the Quickstart with the following:
{-# LANGUAGE QuasiQuotes #-}
import Data.Aeson.QQ (aesonQQ)
import Data.GraphQL.TestUtils (ResultMock(..), mocked, runMockQueryT)
import Example (app)
import Example.GraphQL.API
main :: IO ()
main = do
let mockedGetRecordings = mocked ResultMock
{ query = GetRecordingsQuery
{ _query = "My Song"
, _first = Just 5
}
, result =
[aesonQQ|
{
"search": {
"recordings": {
"nodes": []
}
}
}
|]
}
-- should not hit the server
result <- runMockQueryT app [mockedGetRecordings]
-- test `result`, which should be the result hardcoded above