Type Safe, Functional GraphQL with Purescript - Part 1

GraphQL is a great way to model the underlying data structure of an API. It allows clients to request only the data they want and structures the API as a graph or relational data. However, at this time there is no convenient or complete GraphQL library for Haskell. Why Haskell? A strongly typed, functional language allows us to build systems with a smaller potential for bugs and a greater potential for success. We use Haskell for all our mission critical systems and so with no Haskell library, we looked to other options. Purescript has many similar features as Haskell and compiles to JavaScript. With Purescript, we can code our business logic in a Haskell-like language and take advantage of the great JavaScript libraries for GraphQL.

In this post, I will walk you through how to setup a simple GraphQL API with Purescript resolver functions. I will intentionally gloss over some of the setup that can be found in many other blog posts or documentation.

Setup our GraphQL Server

First, create a new Express.js project and add the GraphQL dependency express-graphql. Create the file src/index.js for your Express.js server and follow their instructions to implement a basic server with the GraphQL middleware. Turn on graphiql so we can play with our API. You should have something like this (taken from express-graphql README):

const express = require('express');
const graphqlHTTP = require('express-graphql');

const app = express();

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true
}));

app.listen(4000);

This won't run without myGraphQLSchema so let's create a separate file named schema.graphql and add the bare minimum for our schema.

type Query {
  test: Boolean
}

Now we can create our schema from that file.

const express = require('express');
const graphqlHTTP = require('express-graphql');
const fs = require('fs');
const graphql = require('graphql');

const app = express();
const myGraphQLSchema = graphql.buildSchema(
    fs.readFileSync('schema.graphql', { encoding: 'utf8' })
);

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true
}));

app.listen(4000);

Run your file node src/index.js and visit localhost:4000 in your browser. You'll see the GraphQL interface. You type your queries on the left and the results appear on the right. It also has auto-complete that is aware of your schema file. Run the query { test }. You should see the result

{
  "data": {
    "test": null
  }
}

The result is null because we haven't provided any resolver functions yet. So let's add our first resolver function.

To do this we need to know a bit more about how GraphQL works. There is an option we can pass in the settings named rootValue. This is the entry point for our GraphQL API. GraphQL looks at this value and traverses it similar to the way we are requesting data. So if we query { test }, it's going to look in the rootValue for the test key. If the test key value is a scalar value (Int, String, etc) it returns that value. If it's a function, it executes the function and returns the result.

Let's test it out. Change your server code to the following.

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true,
  rootValue: { test: true }
}));

Restart your server and query it again { test }. This time it should return { "data": { "test": true } }.

Now try using a function.

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true,
  rootValue: { test: function () { return true; } }
}));

Restart and query again. It should still return { "data": { "test": true } }.

Purescript Time

Now that we know how GraphQL will traverse our root object, all we have to do is write our Purescript code so that it will compile into JavaScript objects, scalars, and functions.

Create a new file called API.purs and let's put our basic test function in it.

module API where

test :: Boolean
test = true

To compile this we'll need purescript and pulp in our dependencies. Run npm install purescript pulp --save-dev. You will also need bower for Purescript dependencies, npm install bower -g. Then create the bower.json file. It should look like this.

{
  "name": "purescript-graphql-api",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "output",
    "test"
  ]
}

Now add the Purescript build script to your package.json file.

"build": "pulp build -O --skip-entry-point --main API --to output/output.js"

Running this will build your Purescript code with optimizations, -O, and use the API module as the main class. It will build all your modules and dependencies individually and then optimize them into a single output file output/output.js. You can view the individual built modules in the output directory if you'd like.

Run the build, npm run build, and take a look at the output.

// Generated by purs bundle 0.11.7
var PS = {};
(function(exports) {
  // Generated by purs version 0.11.7
  "use strict";
  var test = true;
  exports["test"] = test;
})(PS["API"] = PS["API"] || {});

The compiler creates a global object, PS, where it stores all the compiled modules. Unfortunately, at the time of writing this, you cannot import this file into Node.js. We need to add an export at the end of the file module.exports = PS['API'];. We can do that with another script. Modify your package.json scripts to look like this:

"pulp": "pulp build -O --skip-entry-point --main API --to output/output.js",
"build": "npm run pulp && echo \"module.exports = PS['API'];\" >> output/output.js && cp output/output.js src/api.js"

Now our build script builds our Purescript code, appends the export line, and copies it into our source directory so we can easily import it into our index.js file.

Open your index.js file, import api.js, and pass that into your GraphQL rootValue.

const express = require('express');
const graphqlHTTP = require('express-graphql');
const fs = require('fs');
const graphql = require('graphql');
const api = require('./api');              // Import compiled api.js

const app = express();
const myGraphQLSchema = graphql.buildSchema(
    fs.readFileSync('./src/schema.graphql', { encoding: 'utf8' })
);

app.use('/', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true,
  rootValue: api                          // Set api as the rootValue
}));

app.listen(4000);

Run your server, node src/index.js, and visit localhost:4000 again. Execute the query { test } and you should still see { "data": { "test": true } } as the result.

Wow great! You now have a functioning GraphQL API running on Purescript functions! This is a very simple API but shows that it is possible to write code in a type-safe, functional language and interface with a robust, existing GraphQL library. Next time, We'll get into more detailed Purescript functions including effectful functions, interfacing with external JavaScript libraries, testing, and eventually the allusive Free Monad.

Check out the working code on GitHub.