
A First Look at GraphQL Helix
Published:
GraphQL Helix is a runtime agnostic collection of utility functions that helps you build your own GraphQL API and HTTP server.
Outline
- Introduction
- Motivations and API
- Serve GraphQL Helix Locally
- Deploy GraphQL Helix with Serverless Framework
- Deploy GraphQL Helix with Amplify
- Deploy GraphQL Helix with Docker and Fly
- Resources
- Discover Related Articles
All of this project’s code can be found in the First Look monorepo on my GitHub.
Introduction
GraphQL Helix is a framework and runtime agnostic collection of utility functions for building your own GraphQL HTTP server. Instead of providing a complete HTTP server or middleware plugin function, GraphQL Helix only provides a handful of functions for turning an HTTP request into a GraphQL execution result. You decide how to send back the response.
Motivations and API
Daniel Rearden listed the following reasons pushing him to create Helix, believing that these factors were absent from popular solutions like Apollo Server, express-graphql and Mercurius:
- Wanted bleeding-edge GraphQL features like
@defer
,@stream
and@live
directives. - Wanted to not be tied down to a specific framework or runtime environment.
- Wanted control over how server features like persisted queries were implemented.
- Wanted something other than WebSockets (i.e. SSE) for subscriptions.
renderGraphiQL
and shouldRenderGraphiQL
renderGraphiQL
returns the HTML to render a GraphiQL instance. shouldRenderGraphiQL
uses the method and headers in the request to determine whether a GraphiQL instance should be returned instead of processing an API request.
getGraphQLParameters
getGraphQLParameters
extracts the GraphQL parameters from the request including the query
, variables
and operationName
values.
processRequest
processRequest
takes the schema
, request
, query
, variables
, operationName
and a number of other optional parameters and returns one of three kinds of results, depending on how the server should respond:
RESPONSE
- regular JSON payloadMULTIPART RESPONSE
- multipart response (when@stream
or@defer
directives are used)PUSH
- stream of events to push back down the client for a subscription
Serve GraphQL Helix Locally
mkdir ajcwebdev-graphql-helixcd ajcwebdev-graphql-helixyarn init -yyarn add express graphql-helix graphqltouch index.jsecho 'node_modules\n.DS_Store' > .gitignore
index.js
const express = require("express")const { getGraphQLParameters, processRequest, renderGraphiQL, shouldRenderGraphiQL,} = require("graphql-helix")const { GraphQLObjectType, GraphQLSchema, GraphQLString,} = require("graphql")
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: () => ({ hello: { type: GraphQLString, resolve: () => "Hello from GraphQL Helix!", } }), }),})
const app = express()
app.use(express.json())
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 { const { operationName, query, variables } = getGraphQLParameters(request)
const result = await processRequest({ operationName, query, variables, request, schema, })
if (result.type === "RESPONSE") { result.headers.forEach(( { name, value } ) => res.setHeader(name, value)) res.status(result.status) res.json(result.payload) } }})
const port = process.env.PORT || 4000
app.listen(port, () => { console.log(`GraphQL server is running on port ${port}.`)})
Run test queries on GraphQL Helix Locally
Start the server with node index.js
.
node index.js
Open localhost:4000/graphql and send a hello
query.
query HELLO_QUERY { hello }
curl --request POST \ --url http://localhost:4000/graphql \ --header 'content-type: application/json' \ --data '{"query":"{ hello }"}'
GraphQL Helix Final Project Structure
├── .gitignore├── index.js└── package.json
Deploy GraphQL Helix with Serverless Framework
The Serverless Framework is an open source framework for building applications on AWS Lambda. It provides a CLI for developing and deploying AWS Lambda functions, along with the AWS infrastructure resources they require.
mkdir graphql-helix-serverlesscd graphql-helix-serverlessyarn init -yyarn add express graphql-helix graphql serverless-httptouch index.js serverless.ymlecho 'node_modules\n.DS_Store\n.serverless' > .gitignore
index.js
The serverless-http
package is a piece of middleware that handles the interface between Node applications and the specifics of API Gateway. It allows you to wrap your API for serverless use without needing an HTTP server, port, or socket.
const serverless = require('serverless-http')const express = require("express")const { getGraphQLParameters, processRequest, renderGraphiQL, shouldRenderGraphiQL,} = require("graphql-helix")const { GraphQLObjectType, GraphQLSchema, GraphQLString,} = require("graphql")
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: () => ({ hello: { type: GraphQLString, resolve: () => "Hello from GraphQL Helix on Serverless!", } }), }),})
const app = express()
app.use(express.json())
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 { const { operationName, query, variables } = getGraphQLParameters(request)
const result = await processRequest({ operationName, query, variables, request, schema, })
if (result.type === "RESPONSE") { result.headers.forEach(( { name, value } ) => res.setHeader(name, value)) res.status(result.status) res.json(result.payload) } }})
const handler = serverless(app)
module.exports.start = async (event, context) => { const result = await handler(event, context)
return result}
serverless.yml
The resources and functions are defined in a file called serverless.yml
which includes:
- The
provider
for the Noderuntime
and AWSregion
- The
handler
andevents
for yourfunctions
.
service: ajcwebdev-graphql-helix-expressframeworkVersion: '2'
provider: name: aws stage: dev runtime: nodejs14.x versionFunctions: false lambdaHashingVersion: 20201221
httpApi: cors: allowedOrigins: - '*' allowedMethods: - GET - POST - HEAD allowedHeaders: - Accept - Authorization - Content-Type
functions: endpoint: handler: index.start events: - httpApi: path: '*' method: '*'
The handler is named index.start
because it is formatted as <FILENAME>.<HANDLER>
.
Upload to AWS with sls deploy
Once the project is defined in code it can be deployed with the sls deploy
command. This command creates a CloudFormation stack defining any necessary resources such as API gateways or S3 buckets.
sls deploy --verbose
service: ajcwebdev-graphql-helix-expressstage: devregion: us-east-1stack: ajcwebdev-graphql-helix-express-devresources: 10api keys: Noneendpoints: ANY - https://cuml5hnx0b.execute-api.us-east-1.amazonaws.comfunctions: endpoint: ajcwebdev-graphql-helix-express-dev-endpointlayers: None
Run test queries on GraphQL Helix Serverless
Access your graph by adding /graphql
to end of the provided endpoint (cuml5hnx0b.execute-api.us-east-1.amazonaws.com/graphql
in my case) and send a hello
query.
query HELLO_QUERY { hello }
curl --request POST \ --url https://cuml5hnx0b.execute-api.us-east-1.amazonaws.com/graphql \ --header 'content-type: application/json' \ --data '{"query":"{ hello }"}'
GraphQL Helix Serverless Final Project Structure
├── .gitignore├── index.js├── package.json├── serverless.yml└── yarn.lock
Deploy GraphQL Helix with Amplify
AWS Amplify is a set of tools and services to help frontend web and mobile developers build fullstack applications with AWS infrastructure. It includes a CLI for creating and deploying CloudFormation stacks along with a Console and Admin UI for managing frontend web apps, backend environments, CI/CD, and user data.
mkdir graphql-helix-amplifycd graphql-helix-amplifyamplify init
The amplify init
command creates a boilerplate project that is setup for generating CloudFormation templates.
? Enter a name for the project ajcwebdevhelixThe following configuration will be applied:
Project information| Name: ajcwebdevhelix| Environment: dev| Default editor: Visual Studio Code| App type: javascript| Javascript framework: none| Source Directory Path: src| Distribution Directory Path: dist| Build Command: npm run-script build| Start Command: npm run-script start
? Initialize the project with the above configuration? YesUsing default provider awscloudformation? Select the authentication method you want to use: AWS profile
For more information on AWS Profiles, see:https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
? Please choose the profile you want to use default
Create backend with amplify add api
amplify add api
configures a Lambda handler and API gateway to serve the function.
amplify add api
? Please select from one of the below mentioned services: REST? Provide a friendly name for your resource to be used as a label for this category in the project: helixresource? Provide a path (e.g., /items): /graphql? Choose a Lambda source: Create a new Lambda function? Provide the AWS Lambda function name: helixfunction? Choose the function runtime that you want to use: NodeJS? Choose the function template that you want to use: Hello World? Do you want to access other resources created in this project from your Lambda function? N? Do you want to edit the local lambda function now? N? Restrict API access: N? Do you want to add another path? N
cd amplify/backend/function/helixfunction/srcyarn add graphql-helix graphql express serverless-httpcd ../../../../../
index.js
const serverless = require('serverless-http')const express = require("express")const { getGraphQLParameters, processRequest, renderGraphiQL, shouldRenderGraphiQL,} = require("graphql-helix")const { GraphQLObjectType, GraphQLSchema, GraphQLString,} = require("graphql")
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: () => ({ hello: { type: GraphQLString, resolve: () => "Hello from GraphQL Helix on Amplify!", } }), }),})
const app = express()
app.use(express.json())
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 { const { operationName, query, variables } = getGraphQLParameters(request)
const result = await processRequest({ operationName, query, variables, request, schema, })
if (result.type === "RESPONSE") { result.headers.forEach(( { name, value } ) => res.setHeader(name, value)) res.status(result.status) res.json(result.payload) } }})
module.exports.handler = serverless(app)
Upload to AWS with amplify push
amplify push
uploads the stack templates to an S3 bucket and calls the CloudFormation API to create or update resources in the cloud.
amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
┌──────────┬───────────────┬───────────┬───────────────────┐│ Category │ Resource name │ Operation │ Provider plugin │├──────────┼───────────────┼───────────┼───────────────────┤│ Function │ helixfunction │ Create │ awscloudformation │├──────────┼───────────────┼───────────┼───────────────────┤│ Api │ helixresource │ Create │ awscloudformation │└──────────┴───────────────┴───────────┴───────────────────┘
? Are you sure you want to continue? Yes
REST API endpoint: https://acj63jadzb.execute-api.us-west-1.amazonaws.com/dev
Run test queries on GraphQL Helix Amplify
Access your graph by adding /graphql
to the end of the provided endpoint (acj63jadzb.execute-api.us-west-1.amazonaws.com/dev
in my case) and send a hello
query.
query HELLO_QUERY { hello }
curl --request POST \ --url https://acj63jadzb.execute-api.us-west-1.amazonaws.com/dev/graphql \ --header 'content-type: application/json' \ --data '{"query":"{ hello }"}'
GraphQL Helix Amplify Final Project Structure
├── .gitignore└── amplify └── backend ├── api │ └── helixresource │ ├── api-params.json │ ├── helixresource-cloudformation-template.json │ └── parameters.json └── function └── helixfunction ├── function-parameters.json ├── helixfunction-cloudformation-template.json └── src ├── event.json ├── index.js ├── package.json └── yarn.lock
Deploy GraphQL Helix with Docker and Fly
Fly is a platform for fullstack applications and databases that need to run globally. Fly executes your code close to users and scales compute in cities where your app is busiest. You can run arbitrary Docker containers and host popular databases like Postgres.
mkdir graphql-helix-dockercd graphql-helix-dockernpm init -ynpm i express graphql-helix graphqltouch index.js Dockerfile .dockerignore docker-compose.ymlecho 'node_modules\n.DS_Store' > .gitignore
index.js
const express = require("express")const { getGraphQLParameters, processRequest, renderGraphiQL, shouldRenderGraphiQL,} = require("graphql-helix")const { GraphQLObjectType, GraphQLSchema, GraphQLString,} = require("graphql")
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "Query", fields: () => ({ hello: { type: GraphQLString, resolve: () => "Hello from GraphQL Helix on Docker!", } }), }),})
const app = express()
app.use(express.json())
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 { const { operationName, query, variables } = getGraphQLParameters(request)
const result = await processRequest({ operationName, query, variables, request, schema, })
if (result.type === "RESPONSE") { result.headers.forEach(( { name, value } ) => res.setHeader(name, value)) res.status(result.status) res.json(result.payload) } }})
const port = process.env.PORT || 8080
app.listen(port, () => { console.log(`GraphQL server is running on port ${port}.`)})
Dockerfile
Docker can build images automatically by reading the instructions from a Dockerfile
. A Dockerfile
is a text document that contains all the commands a user could call on the command line to assemble an image.
FROM node:14-alpineLABEL org.opencontainers.image.source https://github.com/ajcwebdev/graphql-helix-dockerWORKDIR /usr/src/appCOPY package*.json ./RUN npm iCOPY . ./EXPOSE 8080CMD [ "node", "index.js" ]
For a more in depth explanation of these commands, see my previous article, A First Look at Docker.
.dockerignore
Before the docker CLI sends the context to the docker daemon, it looks for a file named .dockerignore
in the root directory of the context.
node_modulesDockerfile.dockerignore.git.gitignorenpm-debug.log
If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it. This helps avoid sending large or sensitive files and directories to the daemon.
docker-compose.yml
Compose is a tool for defining and running multi-container Docker applications. After configuring your application’s services with a YAML file, you can create and start all your services with a single command.
version: "3.9"services: web: build: . ports: - "49160:8080"
Run test queries on GraphQL Helix Docker
The docker compose up
command aggregates the output of each container. It builds, (re)creates, starts, and attaches to containers for a service.
docker compose up
Attaching to web_1web_1 | GraphQL server is running on port 8080.
To test your app, get the port of your app that Docker mapped:
docker ps
Docker mapped the 8080
port inside of the container to the port 49160
on your machine.
CONTAINER ID50935f5f4ae6
IMAGEgraphql-helix-docker_web
COMMAND"docker-entrypoint.s…"
CREATED47 seconds ago
STATUSUp 46 seconds
PORTS0.0.0.0:49160->8080/tcp, :::49160->8080/tcp
NAMESgraphql-helix-docker_web_1
Open localhost:49160/graphql and send a hello
query.
query HELLO_QUERY { hello }
curl --request POST \ --url http://localhost:49160/graphql \ --header 'content-type: application/json' \ --data '{"query":"{ hello }"}'
Launch app on Fly with fly launch
Run fly launch
in the directory with your source code to configure your app for deployment.
fly launch \ --name graphql-helix-docker \ --region sjc
This will create and configure a fly app by inspecting your source code and prompting you to deploy.
Creating app in /Users/ajcwebdev/graphql-helix-dockerScanning source codeDetected Dockerfile appAutomatically selected personal organization: Anthony CampoloCreated app graphql-helix-docker in organization personalWrote config file fly.tomlYour app is ready. Deploy with `flyctl deploy`? Would you like to deploy now? No
Open fly.toml
and add the following PORT
number under env
.
[env] PORT = 8080
Deploy application with fly deploy
fly deploy
coverImage: src: registry.fly.io/graphql-helix-docker:deployment-1631689218Image size: 124 MB
==> Creating releaseRelease v2 created
You can detach the terminal anytime without stopping the deploymentMonitoring Deployment
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]--> v0 deployed successfully
Check the application’s status with fly status
.
fly status
App Name = graphql-helix-docker Owner = personal Version = 0 Status = running Hostname = graphql-helix-docker.fly.dev
Deployment Status ID = 47cb82b9-aaf1-5ee8-df1b-b4f10e389f16 Version = v0 Status = successful Description = Deployment completed successfully Instances = 1 desired, 1 placed, 1 healthy, 0 unhealthy
InstancesID TASK VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATEDa8d02b87 app 0 sjc run running 1 total, 1 passing 0 4m28s ago
Run test queries on GraphQL Helix Docker Fly
Open graphql-helix-docker.fly.dev/graphql and send a hello
query.
query HELLO_QUERY { hello }
curl --request POST \ --url https://graphql-helix-docker.fly.dev/graphql \ --header 'content-type: application/json' \ --data '{"query":"{ hello }"}'
GraphQL Helix Docker Final Project Structure
├── .dockerignore├── .gitignore├── docker-compose.yml├── Dockerfile├── fly.toml├── index.js└── package.json
Resources
Building a GraphQL server with GraphQL Helix provides a comprehensive description of GraphQL Helix’s implementation. The examples
folder in the graphql-helix
repo also includes example applications such as:
- HTTP Server
- Express
- Fastify
- Koa
- Live Queries
- Persisted Queries
- WebSockets
- Content Security Policy
- Next.js