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.
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:
Node
objectThe 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, calledid
that returns a non-nullID
.Thisid
should be a globally unique identifier for this object, and given just thisid
, 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 theNode
‘sid
field is passed as theid
parameter to thenode
root field.
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:
{ 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.
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.
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
:
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
functionfromGlobalId
function will retrieve the object using its global IDOur 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.
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.
LogRocket is like a DVR for web and mobile 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. Start monitoring for free.Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
2 Replies to "Making a GraphQL server compatible with Relay"
update URL to docs: https://relay.dev/docs/guides/graphql-server-specification/
Thank you! We’ve updated the link to the documentation.