Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

Making a GraphQL server compatible with Relay

7 min read 2169

Making a GraphQL server compatible with Relay

GraphQL is a very powerful technology that enables us to build better APIs. GraphQL is a query language that helps us avoid unnecessary requests to our servers, reduces the over-fetching and under-fetching requests, and only gets exactly the data that we need. The technology has been growing in usage, the adoption of GraphQL in different languages is getting massive and the future is really bright for those who want to build GraphQL APIs.

Relay is a powerful JavaScript framework for declaratively working with GraphQL and building data-driven React applications. It’s not the most used GraphQL client by the community and there are a few reasons for that. One of them being that Relay is more structured and opinionated than other frameworks, the documentation is not very intuitive and the community itself is not very large. Although Apollo has a few advantages over Relay, mainly related to community and documentation, Relay has some advantages that make it really special.

Relay is recommended to use in the frontend to have more structured, modular, future-proofed applications that can scale easily to millions of users. The first thing that needs to be implemented in order to use Relay is to make a Relay-compatible GraphQL server. That’s what we’re going to do now.

GraphQL server specification

Relay works in an elegant way when handling caching and data fetching, it’s one of the biggest advantages that it has over other GraphQL clients.

Relay has something called the GraphQL Server Specification in its documentation, this guide shows the conventions that Relay makes about a GraphQL server in order to work correctly.

When creating a new GraphQL server that’s going to work with Relay, we need to make sure that it follows these principles:

A Node object

The Node interface is used for refetching an object, as the documentation says:

The server must provide an interface called Node. That interface must include exactly one field, called id that returns a non-null ID.This id should be a globally unique identifier for this object, and given just this id, the server should be able to refetch the object.

The Node interface is very important for the GraphQL schema to have a standard way of asking for an object using its ID.

Another thing that needs to be implemented is the Node root field. This field takes only one non-null globally unique ID as an argument:

If a query returns an object that implements Node, then this root field should refetch the identical object when the value returned by the server in the Node‘s id field is passed as the id parameter to the node root field.

How to page through connections

Pagination was always a pain point in APIs and there’s no standard way of implementing it correctly. Relay handles this problem very well by using the GraphQL Cursor Connections Specification:

We made a custom demo for .
No really. Click here to check it out.

{
  podcasts {
    name
    episodes(first: 10) {
      totalCount
      edges {
        node {
          name
        }
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

The spec proposes a pattern called connections, and in the query, the connection provides a standard way for slicing and paginating the results. The connections also provide a standard way for responses providing cursors, a way of telling the client when more results are available.

Predictable mutations

Mutations should be using the input type in order to make them predictable and structured in a standard way:

input AddPodcastInput {
  id: ID!
  name: String!
}

mutation AddPodcastMutation($input: AddPodcastInput!) {
  addPodcast(input: $input) {
    podcast {
      id
      name
    }
  }
}

Making a GraphQL server compatible with Relay will assure that we have a well-structured, performant, and scalable GraphQL API that can be easily scalable to millions.

Getting started

Now that we know what the three principles that Relay expects our GraphQL server to provide, let’s create an example that follows the GraphQL Server Specification and see how it works in practice.

The first thing that we’re going to do is create a new project and install some dependencies:

yarn add @koa/cors @koa/router graphql graphql-relay koa koa-bodyparser koa-graphql koa-helmet koa-logger nodemon

Now we are going to add some dev dependencies:

yarn add --dev @types/graphql-relay @types/koa-bodyparser @types/koa-helmet @types/koa-logger @types/koa__cors @types/node ts-node typescript

After installing all these dependencies, we are now ready to create our GraphQL server. We are going to create a folder called src and here are the files for our example app:

-- src
  -- graphql.ts
  -- index.ts
  -- NodeInterface.ts
  -- schema.ts
  -- types.ts
  -- utils.ts
-- nodemon.json
-- tsconfig.json

Let’s create a file called tsconfig.json in order to use TypeScript in this project. We will put the following code inside this file:

{
  "compilerOptions": {
    "lib": ["es2016", "esnext.asynciterable"],
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "baseUrl": "."
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

In this example, we’re going to use nodemon to watch our src directory and restart our server every time a change is detected inside our directory. Inside the nodemon.json file, put the following code:

{
  "watch": ["src"],
  "ext": "ts",
  "ignore": ["src/**/*.spec.ts", "src/types/**/*.d.ts"],
  "exec": "ts-node ./src/index.ts"
}

Inside our index.ts file, we are going to put the following code:

import Koa from "koa";
import cors from "@koa/cors";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import logger from "koa-logger";
import helmet from "koa-helmet";
import graphqlHTTP from "koa-graphql";

import schema from "./schema";

const app = new Koa();
const router = new Router();

const graphqlServer = graphqlHTTP({ schema, graphiql: true });
router.all("/graphql", bodyParser(), graphqlServer);

app.listen(5000);
app.use(graphqlServer);
app.use(logger());
app.use(cors());
app.use(helmet());
app.use(router.routes()).use(router.allowedMethods());

For now, we just created a simple GraphQL API using Koa. We imported our GraphQL schema from our schema.ts file and now we are going to create the types that we’re going to need for our server to work properly.

Inside our types.ts is where we’re going to put the following code:

export class IUser {
  id: string;
  firstName: string;
  lastName: string;
  constructor(data) {
    this.id = data.id;
    this.firstName = data.firstName;
    this.lastName = data.lastName;
  }
}

We’re also going to create a file called utils.ts and create an empty array called users:

export const users = [];

We just created the type for our user. Now, inside our schema.ts file, we are going to create our GraphQL schema, let’s put the following code inside this file:

import { GraphQLSchema } from "graphql";
import { QueryType, MutationType } from "./graphql";

const schema = new GraphQLSchema({
  query: QueryType,
  mutation: MutationType,
});

export default schema;

Inside our graphql.ts file we are going to create our queries. Let’s create a variable called QueryType and import the NodeField from our NodeInterface.ts file:

import {
  GraphQLObjectType,
  GraphQLInt,
  GraphQLString,
  GraphQLNonNull,
} from "graphql";

import { NodeField, NodesField } from "./NodeInterface";

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "The root of all... queries",
  fields: () => ({
    node: NodeField,
    nodes: NodesField,
  }),
});

Now, we have the basics of our GraphQL server. Let’s implement the Node field and see how it works. Let’s go now to our NodeInterface.ts file and type some code.

We’re going to import functions from graphql-relay, nodeDefinitions, and fromGlobalId:

  • The nodeDefinitions functions help us to map globally defined IDs into actual data objects. The first argument of this function receives the fromGlobalId function and the second argument is used to read the type of the object using fromGlobalId function
  • The fromGlobalId function will retrieve the object using its global ID

Our NodeInterface.ts file is going to look like this:

import { nodeDefinitions, fromGlobalId } from "graphql-relay";
import { UserType } from "./graphql";
import { users } from "./utils";
import { IUser } from "./types";

const { nodeField, nodesField, nodeInterface } = nodeDefinitions(
  async (globalId: string) => {
    const { id: userGlobalID, type } = fromGlobalId(globalId);
    if (type === "User")
      return await users.find(
        ({ id }: IUser) => (id as string) === userGlobalID
      );
    return null;
  },
  (obj) => {
    if (obj instanceof IUser) return UserType;
    return null;
  }
);

export const NodeInterface = nodeInterface;
export const NodeField = nodeField;
export const NodesField = nodesField;

Let’s now create our user type for our GraphQL server. Inside our graphql.ts file, let’s import a few things and create a variable called UserType. Our whole graphql.ts should look like this now:

import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";

import { NodeField, NodesField } from "./NodeInterface";

export const UserType: GraphQLObjectType = new GraphQLObjectType({
  name: "User",
  description: "User",
  fields: () => ({
    firstName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ firstName }) => firstName,
    },
    lastName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ lastName }) => lastName,
    },
  }),
});

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "The root of all... queries",
  fields: () => ({
    node: NodeField,
    nodes: NodesField
  }),
});

For now, we’re not using the global ID principle that Relay expects us to use. We’re going to import the globalIdField from graphql-relay and pass it before our firstName field, like this:

import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";
import { globalIdField } from "graphql-relay";

import { NodeField, NodesField } from "./NodeInterface";

export const UserType: GraphQLObjectType = new GraphQLObjectType({
  name: "User",
  description: "User",
  fields: () => ({
    id: globalIdField("User"),
    firstName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ firstName }) => firstName,
    },
    lastName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ lastName }) => lastName,
    },
  }),
});

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "The root of all... queries",
  fields: () => ({
    node: NodeField,
    nodes: NodesField
  }),
});

Now, we are going to create a new property inside our User type called interfaces and pass our NodeInterface. First, import the NodeInterface from our NodeInterface.ts file and now we are going to add a new property to our UserType, it’s going to look like this:

import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";
import { globalIdField } from "graphql-relay";

import { NodeField, NodesField, NodeInterface } from "./NodeInterface";

export const UserType: GraphQLObjectType = new GraphQLObjectType({
  name: "User",
  description: "User",
  fields: () => ({
    id: globalIdField("User"),
    firstName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ firstName }) => firstName,
    },
    lastName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ lastName }) => lastName,
    },
  }),
  interfaces: () => [NodeInterface],
});

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "The root of all... queries",
  fields: () => ({
    node: NodeField,
    nodes: NodesField
  }),
});

Let’s now import the connectionDefinitions from graphql-relay and create a new variable called UserConnection. We will use this UserConnection variable to create a connection type for our user. We will create the UserConnection inside our graphql.ts.

Also inside our graphql.ts file, we are going to create a new query to get our users called users. We will pass our UserConnection and as an argument, we are going to important the connectionArgs from graphql-relay and pass it. In order to paginate over our results properly, we will import the connectionFromArray and pass our array of users and our arguments.

After all these changes, our graphql.ts looks something like this:

import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";
import {
  globalIdField,
  connectionDefinitions,
  connectionFromArray,
  connectionArgs,
  mutationWithClientMutationId,
} from "graphql-relay";

import { NodeField, NodesField, NodeInterface } from "./NodeInterface";
import { IUser } from "./types";
import { users } from "./utils";

export const UserType: GraphQLObjectType = new GraphQLObjectType<IUser>({
  name: "User",
  description: "UserType",
  fields: () => ({
    id: globalIdField("User"),
    firstName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ firstName }) => firstName,
    },
    lastName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ lastName }) => lastName,
    },
  }),
  interfaces: () => [NodeInterface],
});

export const UserConnection = connectionDefinitions({
  name: "User",
  nodeType: UserType,
});

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "QueryType",
  fields: () => ({
    node: NodeField,
    nodes: NodesField,
    users: {
      type: GraphQLNonNull(UserConnection.connectionType),
      args: {
        ...connectionArgs,
      },
      resolve: (_, args) => connectionFromArray(users, args),
    },
  }),
});

The last thing to do on our server now is create a mutation, we are going to create a mutation called UserCreate where we’re going to create a new user and add it to our array. Our graphql. ts file will look like this now:

import { GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";
import {
  globalIdField,
  connectionDefinitions,
  connectionFromArray,
  connectionArgs,
  mutationWithClientMutationId,
} from "graphql-relay";

import { NodeField, NodesField, NodeInterface } from "./NodeInterface";
import { IUser } from "./types";
import { users } from "./utils";

export const UserType: GraphQLObjectType = new GraphQLObjectType<IUser>({
  name: "User",
  description: "UserType",
  fields: () => ({
    id: globalIdField("User"),
    firstName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ firstName }) => firstName,
    },
    lastName: {
      type: GraphQLNonNull(GraphQLString),
      resolve: ({ lastName }) => lastName,
    },
  }),
  interfaces: () => [NodeInterface],
});

export const UserConnection = connectionDefinitions({
  name: "User",
  nodeType: UserType,
});

export const QueryType = new GraphQLObjectType({
  name: "Query",
  description: "QueryType",
  fields: () => ({
    node: NodeField,
    nodes: NodesField,
    users: {
      type: GraphQLNonNull(UserConnection.connectionType),
      args: {
        ...connectionArgs,
      },
      resolve: (_, args) => connectionFromArray(users, args),
    },
  }),
});

const UserCreate = mutationWithClientMutationId({
  name: "UserCreate",
  inputFields: {
    firstName: {
      type: new GraphQLNonNull(GraphQLString),
    },
    lastName: {
      type: new GraphQLNonNull(GraphQLString),
    },
  },
  mutateAndGetPayload: async ({ firstName, lastName }) => {
    const newUser = { firstName, lastName };
    users.push(newUser);
    return {
      message: "Success",
      error: null,
    };
  },
  outputFields: {
    message: {
      type: GraphQLString,
      resolve: ({ message }) => message,
    },
    error: {
      type: GraphQLString,
      resolve: ({ error }) => error,
    },
  },
});

export const MutationType = new GraphQLObjectType({
  name: "Mutation",
  description: "MutationType",
  fields: () => ({
    UserCreate,
  }),
});

The GraphQL Server Specification assures that you have a very modular, scalable, and robust GraphQL API by making you follow strict rules. In order to use Relay in the frontend, these conventions should be implemented in the backend.

Following the specs makes your code bullet-proofed and easy to scale in the future, you can implement things such as pagination connections and authentication very easily. You can find the code for this article here.

Conclusion

Sometimes working with flexible technologies isn’t the best choice, it can lead to some uncertainty and may not scale in the long-term. Relay is a technology that’s very opinionated and structured, it certainly ensures that you’re using your GraphQL server in the correct way and you won’t have any problem if you follow the conventions.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Leonardo Maldonado Full Stack Developer. JavaScript, React, TypeScript, GraphQL.

Leave a Reply