A Reasonable GraphQL Exploration: Part 3

Putting your GraphQL Handlers to Work with Express

Brandon Konkle
Ecliptic

--

Happy New Year! Now that we’re all rejuvenated after the holiday break, it’s time to dive back into our PaperClip server and wire things in so we can get to work!

In case you missed it, Part 1 of my series introduced ReasonML and how to represent GraphQL types with it, translating back and forth between OCaml types and JSON. Part 2 went over Handlers and Services, translating requests from Apollo’s GraphQL format to ones that my ReasonML Service can understand.

Now, it’s time to plug that Handler into Express so that we can transport these GraphQL requests via HTTP!

Kicking Things Off with Express

To interact with the Node Express library, I’m using a library called bs-express. There is no documentation, but the source code has a good amount of comments to help the reader through. The inconsistent quality of documentation in the Reason/BuckleScript community is definitely something that there is much emphasis on improving, so I’m confident this will change over time.

I’ll start out with the start function in the Server.re module. This function launches an Express server and attaches various middleware to handle requests.

It takes a single labeled argument, graphRouter, which we’ll take a look at in a moment. It attaches the morgan logging middleware with a config appropriate to the current environment. It also attaches the cors and body-parser middleware (with “body-parser” renamed to “json” for semantics).

To handle requests, it attaches the graphRouter middleware. If this is the dev environment, it also includes a graphiqlMiddleware created with the bs-apollo-server-express library. To handle plain HTTP requests, it uses Router.Web, which we’ll also talk about in a moment.

Finally, it starts the Express listener. It passes a quick callback, ~onListen, to be called when the server is online. Here’s what that looks like:

The Startup Process

To kick things off, I like to call my Server.re module from the command line as a Node script. I also want to make my module available to other modules to import, without accidentally triggering another instance of the server. Here’s how I handle that in Reason:

The [%bs.raw] directive tells BuckleScript to output a raw string to the built JavaScript file, allowing me to cheat and handle things that would be much more difficult using the rich external interface it provides. If require.main points to the current module, then this module has been invoked directly on the command line. I automatically call main() in that scenario.

The main() function first initializes a DataProvider instance, and then uses it to create a graphRouter. I’m doing this because I don’t want to have to pass an entire DataProvider into the start routine. This gives me a more granular hook for dependency injection when I want to test the start function.

The catch hook traps promise rejections that aren’t handled elsewhere, and reports them with a Js.log using the Chalk interface to highlight it with color on the command line.

Reasonable Routing

Okay! We’ve got the two major ends of this architecture in place — Handlers and Services are in place and ready to handle GraphQL requests, and Express is in place ready to handle HTTP requests (which GraphQL requests come in through). We need to make these ends meet, routing HTTP requests to appropriate Handlers to translate them to function calls that the Services understand.

Enter the graphRouter I mentioned above:

The router initializes itself in two steps — first making the Schema with the bs-graphql-tools library and then using createGraphQLExpressMiddleware from bs-apollo-server-express to transform that into Express middleware.

Apollo expects the resolvers to handle both the core schema and the queries and mutations — which go under their own top-level Query and Mutation types in the GraphQL schema. To construct this, I use Js.Obj to create an empty object, and then assign the resolvers for each type (in this case, just PaperClip) to the object. Then, I construct a {"Query": ...} and {"Mutation": ...} object to collect those operations for each type. I use the resolvers, queries, and mutations keys from my PaperClipHandler record to access each portion.

Fire it up!

Now it’s time to try it out! I recommend the handy GraphiQL Mac App to play with queries ahead of time before implementing them on your front end. Start the server with yarn on your command line:

The build step runs bsb, the BuckleScript build tool. Then, dev runs the compiled Server.js script like this:

The dotenv library reads in environment variables from a local .env file if needed. Then, I use node to run Server.js directly. If everything was successful, you should see Server is listening on port 4000 in your console, indicating that the server is ready to take requests.

Now you can fire up GraphiQL and point it to http://localhost:4000/graphql.

You’re in business! 🚀

Now what?

Now you can start adding business logic and new features and rely on the strength of the type system to help you think through errors and edge cases in detail before you ever fire up the process!

In Practice

My experience so far has been that the combination of ReasonML and GraphQL gives you a very rich type language to describe your data and interact with your frontend. It’s been much easier to set up a strict, type-safe API with GraphQL than it has been for me with REST in the past. This comes with a tradeoff, though, that may or may not impact you. It is taking me longer to get endpoints production-ready because the type system forces me to think things through very thoroughly.

Once they’re ready, though, they tend to require much less bug-fixing and testing. My error messages are specific and targeted, because the type system forces me to specifically handle every case where an error could occur. This means that my bugs in testing are generally related to invalid assumptions I made while developing the endpoint rather than mistakes in the code itself. This is a powerful tradeoff, but you have to be ready to invest the time to take advantage of it. If you’re working on a speculative project that may end up being discarded, you might not need sound type safety.

Testing

Testing in ReasonML was a wake-up call for me. I’m used to taking messy shortcuts in testing because it’s test-code and will never ship to production. This is much harder to do in ReasonML, and this is likely for the better. I’ve had to take the time to think through useful mocks for my Handlers and Services and implement them in Reason ahead of time for use in the tests — something I really should have been doing in the first place.

Testing is something that we have a strong focus on at Ecliptic, and I’ll be posting an article in the near future about testing in ReasonML with bs-jest. We’re still evolving best practices around testing in this environment, but we have a great deal of experience testing in the traditional React and Node environments. We’re starting to plan out testing workshops as a team, and we hope to be able to offer them soon. Get in touch if you’d like to know more!

Thanks for reading, and I can’t wait to see what you build!

--

--

Brandon Konkle
Ecliptic

Founder and Lead Developer at @eclipticdev, @reasonml acolyte, supporter of social justice, enthusiastic nerd, loving husband & father.