A GraphQL Server in Rescript
Introduction
This is part of an ongoing series about the Rescript programming language. I’m taking notes as I build a toy “Recipe Book” app to explore the Rescript ecosystem.
In my most recent articles, I started investigating the world of offline-enabled apps using RxDB. In an earlier article, I created an express server in Rescript. This article will extend those ideas with a basic graphql server, which we’ll later extend into a syncing backend for RxDB in a later article.
Patreon
This series takes a lot of time to write and maintain. I started it when I was unemployed, and was putting 10 to 20 hours into each article. The articles are shorter now and I don’t work on them every weekend.
If you want to help keep me invested in writing this series, I’ve set up a Patreon account. I thank you for your support. I am not expecting this to pay for my time investment, but it helps to know if people appreciate it enough to make a contribution.
As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.
Other ways to show support include sharing the articles on social media, commenting, or a quick thank you on the Rescript forum.
Other articles in series
With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.
Graphql Server?
I gotta tell you: There are a LOT of choices for implementing a graphql server. There are several options we could explore:
- Use reason-graphql, a pure-reason graphql server that runs on nodejs. This would probably be my default choice if it supported subscriptions and the port to Rescript syntax was completed. Neither of these is true, however, so I’m going to pass on it for now.
- Use the production-ready ocaml-graphql-server and build a native app in ReasonML instead of Rescript. This is the solution that I am most interested in exploring, but I’m not quite ready for this series to diverge into ReasonML.
- Write our own Rescript bindings to one of several Graphql server libraries
that run on nodejs. This is the way I’m going to go. My recent experience
writing Rescript to JS bindings was easy enough that I’m comfortable with it.
Having decided to go this route, we still need to decide which of several
nodejs graphql servers to bind to. Some of the top options are:
- Apollo Server. I tend to shy away from Apollo, mostly because the documentation can be hard to follow.
- graphql-yoga from prisma-labs. I’m going to pass on this one partially because Prisma has a history of not maintaining their stuff, and partially because it’s not clear how to integrate it with the express infrastructure we’ve already written.
- The GraphQL.js reference implementation with graphql-express. I’m going with this because it’s supported by Facebook, has the most stars on github, and, most importantly, allows me to leverage the express server we’ve already written.
Let’s begin
We’ll be extending the rescript-express-recipes repo developed in an earlier article.
If you haven’t been following along with these articles, you can git checkout
the
rescript-express-tutorial
branch to start from the same spot.
If you want to keep up with the commits for this article, they are in the graphql branch. Each commit roughly maps to a section in this article. You may find it instructive to see the diff of the JS that the rescript compiler outputs with each change in the rescript code.
You may want to npm install --upgrade bs-platform
, as I started the older
article on bs-platform 8.4.2 (I’m on 9.0.1 at the time of writing).
Dependencies
We’ll need two new Javascript dependencies. Since we’ll be writing our own
bindings for the Rescript side, we won’t need to update bsconfig.json
.
Install the two deps:
npm install --save express-graphql graphql
npm run build
Port graphql “Hello World”
Before we start building our own graphql resolvers, create a new file named
Schema.res
with a binding to buildSchema
and the “Hello World” of graphql:
type t
@module("graphql") external buildSchema: string => t = "buildSchema"
@module("graphql")
external graphql: (t, string, 'rootValue) => Js.Promise.t<'result> = "graphql"
let schema = buildSchema(`
type Query {
hello: String
}
`)
The bindings should not be too surprising if you read my mini-series on modeling
JS for an offline app. We create a new type t
which represents a schema, and
then a constructor function that returns that type.
At this point, we don’t care about any methods or attributes on that type, so
we just leave it as the bare type, t
.
For the binding to graphql
, we know from the graphql docs
that it is a function that requires a schema and request string, and a bunch
of optional values. For now, I only bothered modeling the rootValue
argument,
and I didn’t even model it well: I just assigned it a generic type.
The let schema
declaration is a direct copy from the express
graphql
documentation.
The string schema tells graphql what type our resolvers must have, but it
doesn’t tell Rescript anything about the types. We can add a new resolvers
record type that specifies the functions for each of the items in the schema.
Unfortunately, it will be up to us to ensure the graphql string schema and the
Rescript schema are in sync.
This new resolvers
type will be a record with functions as values. I didn’t actually
know it was possible to have a record with functions as values, as it is more
an object-oriented than functional model. But this type actually compiles:
type rootValue = {hello: unit => string}
Put that near the top of the file so that you can change the graphql
let
binding below it to accept that type explicitly:
@module("graphql")
external graphql: (t, string, rootValue) => Js.Promise.t<'result> = "graphql"
Let’s create Resolvers.res
to provide the actual implementation of that type:
let hello = () => {
"hello world"
}
let rootValue: Schema.rootValue = {
hello: hello,
}
Before we go any further, let’s test the graphql
function with some temporary
code. I just added this at the top of the existing Index.res
as a quick
test:
let r = Schema.graphql(Schema.schema, "{hello}", Resolvers.rootValue) |> Js.Promise.then_(r => {
r["data"]["hello"]->Js.log
Js.Promise.resolve()
})
This calls the graphql
function we wrote a binding for in Schema.res
. We
pass in the schema
and a query that we want to execute. The third argument is
the rootValue
containing the hello
function resolver we wrote. The return
value of this function is a Promise
. If you were following along with my RxDB
articles, you might recall that they depended on a third-party
rescript-promise
library. In this case, becasue this was just temporary code,
I used the built-in Js.Promise
library instead. The main difference is the
use of |>
pipe-last syntax instead of Rescript’s prefered ->
pipe-first syntax.
Now if you run npm start
, in addition to loading up the express server, it
will output hello world
on the console.
Creating the graphql express endpoint
Of course, running graphql on the console is pretty boring, so let’s next model
the graphql-express
endpoint so we can query from the browser. This took me
quite a while to puzzle out, but it’s surprisingly simple once it works.
First, have a look at the getting started
for running a graphql server in express (in Javascript). The important bit
is the following app.use
:
app.use(
"/graphql",
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
})
);
Our goal is to be able to do the same in Rescript. In order to do that, we need
to model the graphqlHTTP
function. But first, we need to model the options
that graphqlHTTP
can take. As usual, I’m just going to model the subset of
options that I actually use. Create a new GraphqlExpress.res
file with this
type:
type graphQlHttpOptions = {
schema: Schema.t,
graphiql: bool,
rootValue: Schema.rootValue,
}
The actual binding to graphqlHttp
is obvious once you know the trick, but that
trick took me a bit of digging to discover. To work with the bs-express
App.use
, it needs to be wrapped in an Express.Middleware
. I had to poke
around quite a bit to realize that it actually is an Express.Middleware
.
So all I had to do was bind the function as though it were returning that:
@module("express-graphql")
external graphqlHttp: graphQlHttpOptions => Express.Middleware.t = "graphqlHTTP"
The final puzzle is that in bs-express
, App.use
does not accept a path.
Instead, it provides a App.useOnPath
function. Underneath, it’s binding to
the same use
method, but Rescript’s strict typing guarantees mean a separate
name was required. The actual endpoint is similar to the App.get
and
App.post
endpoints I created in the earlier express article:
App.useOnPath(
app,
~path="/graphql",
GraphqlExpress.graphqlHttp({
schema: Schema.schema,
graphiql: true,
rootValue: Resolvers.rootValue,
}),
)
It calls the GraphqlExpress.graphqlHttp
function with the appropriate options
and mounts it on the appropriate path. Since graphiql
is set to true, we get
a web interface to perform Graphql queries. Start the server and visit
localhost:3000.
You should get the standard graphiql
user interface. Paste in the following
query and click the play button:
{
hello
}
If all goes well, you should see the resolved value in the response:
{
"data": {
"hello": "hello world"
}
}
A (slightly) more complex resolver
Just to make sure that everything works when we supply parameters, let’s
also add a greetByName
function.
First update the schema
in Schema.res
:
let schema = buildSchema(`
type Query {
hello: String
greetByName(name: String): String
}
`)
Second, we need to update the Schema.res
rootValue
type for this new
resolver. However, the arguments passed to greetByName
are wrapped in a
Javascript object by the graphql library. We need to model that object as
greetByNameArgs
, not just a string argument:
type greetByNameArgs = {name: string}
type rootValue = {
hello: unit => string,
greetByName: greetByNameArgs => string,
}
Then we need to supply the actual implementation of this resolver in
Resolvers.res
:
let greetByName = ({name}: Schema.greetByNameArgs) => {
return`Hello ${name}`
}
Destructuring syntax to extract the name from the greetByNameArgs
type makes
the resolver looks pretty clean.
Finally, we have to add this resolver to the rootValue
assignment in Resolvers.res
:
let rootValue: Schema.rootValue = {
hello: hello,
greetByName: greetByName,
}
Restart your server and you should be able to run a query like this:
{
greetByName(name: "Terry")
}
and get a response like this:
{
"data": {
"greetByName": "Hello Terry"
}
}
Conclusion
I was originally surprised when I read the Rescript suggests writing your own
JS bindings instead of bringing in a huge ecosystem of types (a la
DefinitelyTyped for typescript). But now that I know how to do that binding, I
actually find it preferable to learning how someone else made their bindings.
When I do my own bindings, I can cheat on some types. For example, the
graphql
function can take any object as a root resolver, but I forced it to
be of type rootValue
, which is unique to my needs. This is easier to work
with than a generic type, and I don’t have to rely on somebody else to maintain
an entire ecosystem of third-party libraries.
Don’t get me wrong; I’m glad I don’t have to bind directly to React, and writing my own express bindings would have been a pain compared to using bs-express. But writing bindings in Rescript is much less convoluted than in Typescript, and you get the flexibility of being as strict or relaxed in your typing as you need to be.
This article covered writing graphql query resolvers in Rescript and hooking up the express endpoint. In the next one, I’ll add mutations to the mix.