Awesome
ReasonQL: GraphQL in ReasonML way.
ReasonQL is a type-safe and simple GraphQL library for ReasonML developers.
ReasonQL does 2 things for you:
- fetch GraphQL data from server.
- and decode that data and error messages from JSON to ReasonML record.
You might think it's too simple and you're now finding cool features like cache, "fetch more", reload, auth, etc. ==> If so, please check "Why I started this project".
Installation.
You need 2 packages: @reasonql/core
and @reasonql/compiler
. Install it with npm like below:
npm i @reasonql/core
npm i -D @reasonql/compiler
or with yarn
yarn add @reasonql/core
yarn add @reasonql/compiler --dev
And add @reasonql/core in bs-dependencies
under bsconfig.json.
"bs-dependencies": [
"@reasonql/core",
"reason-react",
]
How to use ReasonQL.
This document assumes that you're familiar with ReasonML and GraphQL. If you're not sure what GraphQL is, check the offical documentation.
1. Write Query in a Reason file.
let query = ReasonQL.gql({|
query AppQuery {
hello {
message
}
}
|})
Don't forget to use ReasonQL.gql
function and {||}
multiline string. ReasonQL compiler uses them to find if a file has graphql code or not.
WARNING: The query name (AppQuery
above) is used for the name of the file generated by the compiler. So, do not use duplicate query names. Compiler doesn't warn this and multiple queries will try to overwrite a single type file.
2. Set up compiler.
To compile GraphQL queries, we need the path to the GraphQL schema file. Create reasonql.config.js
at the project root and fill it like below:
module.exports = {
schema: "path/to/schema.js",
}
3. Compile the query.
Add the below command to package.json
:
"scripts": {
"reasonql": "reasonql-compiler"
}
And run the command with npm run reasonql
.
You can find AppQuery.re under src/.reasonql
.
Note: As the files under src/.reasonql
are generated by the ReasonQL Compiler, it is recommended to ignore the folder in .gitignore.
Note2: It is a really tedious job to type in npm run reasonql
each time when queries are changed. So, when in development, use the option, -w
, like below:
"scripts": {
"reasonql": "reasonql-compiler",
"reasonql:dev": "reasonql-compiler -w"
}
It watches reason files and regenerate files only when the GraphQL queries are changed.
4. Create Request
module.
module Request = ReasonQL.MakeRequest(AppQuery, {
let url = "http://localhost:4000";
});
MakeRequest
functor receives 2 arguments: a module generated by the ReasonQL compiler and a module that contains the link to the server.
5. Send request and handle the response.
Request.send(Js.Dict.empty())
->Request.finished(data => {
Js.log("Data fetched.");
})
Js.log("Loading data...");
All you need to remember are these 3 functions:
send(argumentRecord)
finished(promise, data => unit)
finishedWithError(promise, (data, option(error)) => unit)
As send
returns a Js.Promise
and finished
and finishedWithError
have promise as their first argument, we can use the pipe syntax here.
You learned the basics of ReasonQL. Unlike other libraries like Apollo or Relay, you don't need to create React components to use GraphQL.
If you want to know how to make "hello world" with ReasonQL, check the example.
Other Features
If you want to see the working examples, check the snippets folder.
Type Conversions
5 scalar types (ID
, Int
, Float
, String
, Boolean
) of GraphQL are converted into appropriate ReasonML types(string
, int
, float
, string
, bool
). (By definition, ID
type is serialized into STRING
.)
Object types are compiled into ReasonML types.
# Schema
type Greeting {
hello: String!
}
type Test1 {
a: Greeting!
}
# Query
query AppQuery {
a {
hello
}
}
type a = {
hello: string,
};
type queryResult = {
a: a,
};
Note: As you can see, object type name doesn't follow the actual name(greeting
) in the type definition, but uses the variable name(a
) to avoid type name conflict. More about name conflict below.
Nullability
In GraphQL, the field types without !
are nullable. So, they're translated into option
-ed types in ReasonML.
Example:
# Schema
type Query {
a: String
b: String!
}
# Query
query Test1 {
a
b
}
type queryResult = {
a: option(string),
b: string,
}
Note: When a type is an option
, we need to use pattern matching to cover all cases. In ReasonML, it's tedious and sometimes meaningless. So, when you define the schema for your app, always consider when null
should be used. If you cannot find the meaningful case, add !
. (Unlike many JavaScript examples, you'll find yourself adding many non-null types in ReasonML apps.)
Enum types
Conventionally, GraphQL enum values are written in all capital with underscores like EXTRA_LARGE
. And they're strings internally.
However, ReasonML uses camel case with the first letter capital-cased. And they're compiled into numbers.
ReasonQL compiler translates that perfectly.
enum PatchSize {
SMALL
MEDIUM
LARGE
EXTRA_LARGE
}
type patchSize =
| Small
| Medium
| Large
| ExtraLarge
;
Unlike other types, encoders and decoders of enum types are defined in EnumTypes.re
file. And the functions are imported to each type file.
Name conflict and renaming types
Sometimes, field names of object types can conflict like below:
# Schema
type Query {
hero: Person!
villain: Person!
}
type Person {
name: Name!
ship: Ship
}
type Name {
first: String!
last: String
}
type Ship {
name: String!
}
# Query
query AppQuery {
hero {
name @reasontype(name:"heroName") {
first
last
}
ship {
name
}
}
villain {
name {
first
}
ship @reasontype(name:"villainShip") {
name
}
}
}
type heroName = {
first: string,
last: option(string),
};
type hero_ship_Ship = {
name: string,
};
type hero = {
name: heroName,
ship: option(hero_ship_Ship),
};
type villain_name_Name = {
first: string,
};
type villainShip = {
name: string,
};
type villain = {
name: villain_name_Name,
ship: option(villainShip),
};
type queryResult = {
hero: hero,
villain: villain,
};
Both hero and villain have name
and ship
. In those cases, the type names are generated with the list of the names in the path and schema type name(i.e. hero_ship_Ship
, villain_name_Name
).
To avoid this, you can use @reasontype
directive like @reasontype(name:"villainShip")
.
Define singular name.
Sometimes, it is logical to name a variable in plural and its type in singular like below:
# Schema
type Query {
posts: [Post!]!
}
type Post {
title: String!
slug: String!
content: String!
summary: String
}
# Query
query AppQuery {
posts @singular(name:"post") {
title
slug
content
summary
}
}
type post = {
title: string,
slug: string,
content: string,
summary: option(string),
};
type queryResult = {
posts: array(post),
};
Automatically merges fragments into the main query.
Borrowed the idea from Relay. When writing code, it is always a good idea to put related things together in one place. With fragment
s, you can define the data a component needs in the same file like below:
let query = ReasonQL.gql({|
fragment PostFragment_post on Post {
title
summary
slug
...ButtonFragment_post
}
|})
let component = ReasonReact.statelessComponent("Post")
let make = (
~post: PostFragment.post,
_children
) => {
...component,
/* Code here */
}
You can read the full code here.
Then, ReasonQL compiler magically merges fragments into the main query.
Mutation and Arguments
Mutations work in the same way like queries. Use MakeRequest
functor and send
, finished
functions. But in mutations, you need to use arguments a lot.
When there are arguments, the type of the argument of send
function changes from Js.Dict
to a specific variables.
So, we need to write code like below:
let saveTweet = ReasonQL.gql({|
mutation SaveTweetMutation($tweet: TweetInput!) {
saveTweet(tweet: $tweet) {
success
id
tempId
text
}
}
|})
module SaveTweet = ReasonQL.MakeRequest(SaveTweetMutation, Client);
SaveTweet.send({
tweet: {
text: tweet.text,
tempId: tweet.id,
}
})
->SaveTweet.finished(data => {
Js.log("data recieved");
})
You can read the full code here.
Errors
Apollo Server provides a lot of error types. Among them, you need to provide your own type definition for UserInputError
. So, we need decoders for those errors.
To do so, create special GraphQL schema file: errors.graphql
.
And write down error types like below:
type LoginFormError {
code: String!
email: String
password: String
}
And add the path to errors.graphql
to reasonql.config.js
like below:
module.exports = {
schema: "../server/src/schema.js",
errors: "../server/src/errors.graphql",
}
Then, the decoder will be generated at .reasonql/QueryErrors.re
. Now, you can decode error contents like below:
Login.send({ email, password })
->Login.finishedWithError((result, errors) => {
switch(errors) {
| None => {
login(Belt.Option.getExn(result.login));
ReasonReact.Router.push("/");
}
| Some(errors) => {
let {email, password}: QueryErrors.loginFormError
= QueryErrors.decodeLoginFormError(errors[0].extensions);
self.send(ShowError(email, password));
}
}
})
Compiler options
Commandline options
-w
,--watch
: watch reason files and generate reasonql type files only when query code changes.
Config file options
schema
: required. The path to the GraphQL schema file.errors
: The path to the error schema file.src
: The root path of the reasonml files. Default:./src
.include
: The files should be included from compilation. Default:**
.exclude
: The files should be excluded from compilation. Default:[ '**/node_modules/**', '**/__mocks__/**', '**/__tests__/**', '**/.*/**', ]
watch
: Watch files. Default:false
.
Contribution
Helps are always welcome. If you want to check how to contribute to the project, check this document.