Using GraphQL union type in a Node.js application

Using GraphQL union type in a Node.js application
Photo by Hugo ROUQUETTE / Unsplash

Photo by Hugo ROUQUETTE / Unsplash

GraphQL supports primitives types and allows you to create custom types for your use cases. One interesting thing about these custom types is combining many GraphQL types to create a new one.

This is common when you need to manipulate two or many objects that have the same business purpose but is different in some properties.

The use case

We want to build a cloud storage system where users can upload their files and organize them into folders. The application will allow users to search files and folders for quick access.

A simple diagram of the entities necessary for the system will look like this:

Simple entity-relationship diagram of the system.
The simple entity-relationship diagram of the system.

We want to build the GraphQL API that allows users to search for files and folders. We will see how to use the GraphQL unions to do that.

Prerequisites

You must need these tools installed on your computer to follow this tutorial:

  • Node.js 14+ - Download's link
  • NPM or Yarn - I will use Yarn
  • A working GraphQL server running on Node.js; we built on the blog post below.
Create a GraphQL application with Node.js and Apollo server 3
In this tutorial, we will create a GraphQL in Node.js using Apollo server 3 then using the new Apollo sandbox to test our queries and mutations.

Set up the project

We will clone the project of the article above and make it works locally. Run the commands below:


git clone https://github.com/tericcabrel/blog-tutorials

cd blog-tutorials/node-graphql

yarn install

yarn start

Navigate to the URL http://localhost:4000/graphql, and you will see the Apollo studio explorer.

Sending request the to the GraphQL server from Apollo Studio explorer.
Sending request to the GraphQL server from Apollo Studio explorer.

If you want to clone a specific folder of a git repository, I made this blog post that can help you achieve that with the sparse-checkout command of Git.

Define the GraphQL schema types

Based on the diagram we built earlier, below is the GraphQL type for each entity:


type User {
  id: ID!
  name: String!
  email: String!
  password: String!
}

type Folder {
  id: ID!
  name: String!
  description: String
  path: String!
  parent: Folder
  author: User!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type File {
  id: ID!
  name: String!
  description: String
  mimeType: String!
  size: Int!
  folder: Folder!
  author: User!
  createdAt: DateTime!
  updatedAt: DateTime!
}

We can now define the GraphQL query definition for searching files and folders:


type Query {
  searchOnFiles(keyword: String!): File!
  searchOnFolders(keyword: String!): Folder!
}

We are forced to define two queries, but we want a single query that can return both File and Folder at the same time.

We will create a union type that can be a File or a Folder at the same time.


union Document = File | Folder

Now, we can define a single query for searching files and folders:


type Query {
  search(keyword: String!): [Document!]!
}

Generate the Typescript types from the GraphQL schema by running the following command:


yarn graphql-codegen --config codegen.yml

To learn more about GraphQL code generator and its purpose, check out this link.

Resolve document type

When a document is found, Apollo must guess if it's a Folder or a File. There is a resolver method named __resolveType() that is responsible for that.

It returns a string with the name of the possible types ("File" or "Folder").

In the file src/resolvers.ts, add the code below:


const resolvers: Resolvers = {
  Document: {
    __resolveType: (obj, _context, _info) => {
      // Only Folder has a "path" field
      if (obj.path) {
        return 'Folder';
      }

      // Only File has a "mimeType" field
      if (obj.mimeType) {
        return 'File';
      }

      return null;
    },
  },
  // ..... rest of code here  
};

  • The type Folder has the property path that the type File doesn't, so if the object has that property, it is definitely a folder.
  • The type File has the property mimeType that the Folder doesn't.

If the value returned is not "File" or "Folder", the Apollo server will throw an error because the latters are the only correct type that forms the union Document.

Write the resolver for the search query

This resolver involves retrieving documents from a data source, and for the sake of brevity, I will skip this part, but you can check out the final code to see the implementation of the data source.

Here is the final code of the file src/resolvers.ts:


import { QueryResolvers, Resolvers } from './types/types';
import { dateTimeScalar } from './types/datetime';
import datasource from './datasource';

const searchDocuments: QueryResolvers['search'] = async (parent, args) => {
  return datasource.searchDocuments(args.keyword);
};

const resolvers: Resolvers = {
  DateTime: dateTimeScalar,
  Document: {
    __resolveType: (obj, _context, _info) => {
      // Only Folder has a name field
      if (obj.path) {
        return 'Folder';
      }

      // Only File has a title field
      if (obj.mimeType) {
        return 'File';
      }

      return null;
    },
  },
  File: {
    author: (file) => {
      return datasource.findUser(file.authorId);
    },
    folder: (file) => {
      return datasource.findFolder(file.folderId);
    },
  },
  Folder: {
    author: (file) => {
      return datasource.findUser(file.authorId);
    },
    parent: (file) => {
      return datasource.findFolder(file.folderId);
    },
  },
  Query: {
    search: searchDocuments,
  },
};

export default resolvers;

Run the search query

The code below shows how to query data that returns a list of union-type Documents:


query seach($keyword: String!) {
  search(keyword: $keyword) {
    ... on File {
      id
      name
      description
      createdAt
      mimeType
      size
      author {
        id
      }
      folder {
        id
      }
    }
    ... on Folder {
      id
      name
      description
      createdAt
      path
      author {
        id
      }
      parent {
        id
      }
    }
  }
}

Re-run the server and go to Apollo studio explorer, copy and paste the code above and execute the query:

Execute the query in Apollo Studio Explorer.
Execute the query in Apollo Studio Explorer.

We successfully retrieve files and folders in a single query from the server.

Wrap up

We saw how GraphQL unions can help solve data modeling in our GraphQL schema. You can go further by checking out how to use GraphQL interfaces to share similar properties between many types in your schema.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.