Earlier this week I released GraphQL Helix, a new JavaScript library that lets you take charge of your GraphQL server implementation.
There's a couple of factors that pushed me to roll my own GraphQL server library:
- I wanted to use bleeding-edge GraphQL features like
@defer,@streamand@livedirectives. - I wanted to make sure I wasn't tied down to a specific framework or runtime environment.
- I wanted control over how server features like persisted queries were implemented.
- I wanted to use something other than WebSocket (i.e. SSE) for subscriptions.
Unfortunately, popular solutions like Apollo Server, express-graphql and Mercurius fell short in one or more of these regards, so here we are.
Existing libraries like Apollo Server provide you with either a complete HTTP server or else a middleware function that you can plug into your framework of choice. GraphQL Helix takes a different approach -- it just provides a handful of functions that you can use to turn an HTTP request into a GraphQL execution result. In other words, GraphQL Helix leaves it up to you to decide how to send back the response.
Let's see how this works in practice.
A Basic Example
We'll start by building an express application and adding a /graphql endpoint.
import express from "express";
import { schema } from "./my-awesome-schema";
const app = express();
app.use(express.json());
app.use("/graphql", async (res, req) => {
// TODO
});
app.listen(8000);
Note that we're assuming here we already have a GraphQL schema we've created. However you build your schema (GraphQL Tools, TypeGraphQL,
graphql-compose, GraphQL Nexus, etc.) is irrelevant -- as long as you have a GraphQLSchema object, you're good to go.
Next, let's extract the relevant bits from our request into a standard GraphQL Helix object:
app.use("/graphql", async (res, req) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
});
More astute readers might note that we could have just used the req object as-is — and that's true! However, this step will look a little different depending on the framework or runtime we use, so I'm being more explicit about how we define this object.
Now let's extract the relevant parameters from the request and process them.
import {
getGraphQLParameters,
processRequest
} from "graphql-helix";
...
app.use("/graphql", async (res, req) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
const {
query,
variables,
operationName
} = getGraphQLParameters(request);
const result = await processRequest({
schema,
query,
variables,
operationName,
request,
})
});
processRequest still takes our Request object as a parameter, so why doesn't it just call getGraphQLParameters for us? As we'll see later on, this is an intentional design choice that gives us the flexibility to decide how the parameters are actually derived from the request.
So, we've processed our request and now have a result. Groovy. Let's do something with that result.
app.use("/graphql", async (res, req) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
const {
query,
variables,
operationName
} = getGraphQLParameters(request);
const result = await processRequest({
schema,
query,
variables,
operationName,
request,
})
if (result.type === "RESPONSE") {
result.headers.forEach(({ name, value }) => {
res.setHeader(name, value)
});
res.status(result.status);
res.json(result.payload);
} else {
// TODO
}
});
Our result includes the headers we should send back, an HTTP status code and the response payload (i.e. an object containing the data and errors we get by actually validating and executing the request).
And that's it! We now have a working /graphql endpoint that can process our requests. Neat.
So why are we writing all this extra boilerplate when I could do the same thing in a few lines of code in Apollo Server? In a word: flexibility. If we swap out Express for another framework like Fastify, we only have to change how we construct our request object and how we handle the result. In fact, we could use the meat of our implementation in virtually any other runtime -- serverless, Deno or even in the browser.
Moreover, we can process the result however our business needs dictate. We have a GraphQL over HTTP specification, but if for some reason you need to deviate from it, you can. It's your application -- send back the status, headers or response that are right for your use case.
So... what's up with that else block? As it turns out, processRequest will return one of three types of results:
-
RESPONSEfor standard queries and mutations, -
MULTIPART_RESPONSEfor requests that include the new@deferand@streamdirectives, and -
PUSHfor subscriptions
Again, it's up to us to implement how to send back these responses, so let's do that now!
Subscriptions
We'll implement our subscriptions using Server Sent Events (SSE). There's a lot of advantages of using SSE over something like WebSockets for subscriptions, like being able to use the same middleware for all your requests, but a deeper comparison of the two approaches will be the topic of a future article.
There's a few libraries out there that can make integrating SSE with Express easier, but we'll do it from scratch for this example:
if (result.type === "RESPONSE") {
...
} else if (result.type === "PUSH") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"Cache-Control": "no-cache",
});
req.on("close", () => {
result.unsubscribe();
});
await result.subscribe((result) => {
res.write(`data: ${JSON.stringify(result)}\n\n`);
});
}
Here, our result includes two methods -- subscribe and unsubscribe. We call subscribe with a callback that's passed the result each time a new subscription event is pushed -- within this callback, we just write to the response with a SSE-compatible payload. And we call unsubscribe when the request is closed (i.e. when the client closes the connection) to prevent memory leaks.
Easy, peasy. Now let's take a look at MULTIPART_RESPONSE.
Multipart Responses
If our request includes @stream or @defer directives, our request needs to be sent down to the client in chunks. For example, with @defer, we send down everything except the deferred fragment and eventually send down the deferred fragment data when its finally resolved. As such, our MULTIPART_RESPONSE result looks a lot like the PUSH result with one key difference -- we do want to eventually end our response once all parts have been sent.
if (result.type === "RESPONSE") {
...
} else if (result.type === "PUSH") {
...
} else {
res.writeHead(200, {
Connection: "keep-alive",
"Content-Type": 'multipart/mixed; boundary="-"',
"Transfer-Encoding": "chunked",
});
req.on("close", () => {
result.unsubscribe();
});
await result.subscribe((result) => {
const chunk = Buffer.from(
JSON.stringify(result),
"utf8"
);
const data = [
"",
"---",
"Content-Type: application/json; charset=utf-8",
"Content-Length: " + String(chunk.length),
"",
chunk,
"",
].join("\r\n");
res.write(data);
});
res.end("\r\n-----\r\n");
}
Note that the Promise returned by subscribe won't resolve until the request has been fully resolved and the callback has been called with all the chunks, at which point we can safely end our response.
Congrats! Our API now has support for @defer and @stream (provide you're using the correct version of graphql-js).
Adding GraphiQL
GraphQL Helix comes with two additional functions that can be used to expose a GraphiQL interface on your server.
shouldRenderGraphiQL takes a Request object and returns a boolean that indicates, as you may have already guessed, whether you should render the interface. This is helpful when you have a single endpoint for both your API and the interface and only want to return the GraphiQL interface when processing a GET request from inside a browser.
renderGraphiQL just returns a string with the HTML necessary to the render the interface. If you want to create a separate endpoint for your documentation, you can use this function without using shouldRenderGraphiQL at all.
app.use("/graphql", async (req, res) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
if (shouldRenderGraphiQL(request)) {
res.send(renderGraphiQL());
} else {
// Process the request
}
});
The returned GraphiQL has a fetcher implementation that will work with multipart requests and SSE as shown in the examples above. If you need to do something else for your server, you can roll your own using renderGraphiQL as a template only.
Evolving your server implementation
GraphQL Helix is, by design, light-weight and unopinionated. Libraries like Apollo Server are bloated with a lot of features that you may never need.
However, that doesn't mean you can't add those features back if you need them. For example, we can add uploads to our server by adding the Upload scalar and using the appropriate middleware from graphql-upload
import { graphqlUploadExpress } from "graphql-upload";
app.use(
"/graphql",
graphqlUploadExpress({
maxFileSize: 10000000,
maxFiles: 10,
}),
(req, res) => {
// Our implementation from before
}
)
Similarly, we can add support for live queries with the @live directive by adding @n1ru4l/graphql-live-query and @n1ru4l/in-memory-live-query-store. We just need to add the directive to our schema and provide the appropriate execute implementation:
import {
InMemoryLiveQueryStore
} from "@n1ru4l/in-memory-live-query-store";
const liveQueryStore = new InMemoryLiveQueryStore();
...
const result = const result = await processRequest({
schema,
query,
variables,
operationName,
request,
execute: liveQueryStore.execute,
});
Tracing, logging, persisted queries, request batching, response deduplication and any number of other features can be added just as easily without the bloat and without having to wrestle with some plugin API or unfriendly abstraction.
You can check the repository for more examples and recipes (I'll be adding more as time allows and also accepting PRs!).
Conclusion
So when should you use Apollo Server instead of GraphQL Helix? If you need to throw together a quick POC or tutorial, Apollo Server is great. If you want to use federation, you might want to stick with Apollo (and even then there are better alternatives to doing GraphQL with microservices).
GraphQL Helix offers a flexible, extensible approach to building a GraphQL server, without the bloat. If you're building something other than another to-do tutorial, I highly recommend checking it out :)

Top comments (6)
If this existed before the Apollo I'd certainly use it everywhere.
What is the difference between this and
graphql-yoga?the-guild.dev/graphql/yoga-server/...
You said it's not tied down to any specific framework, so it's possible to use fastify in place of express right?
do you have this in a repo I can pull down ?
can we run this in a gateway mode?