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.

With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.

Table of Contents

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.