Create GraphQL Api using GraphQL Modules and Apollo Server

We will cover the topic of creating a GraphQL API having a REST API as its data source. For this we decided to use TypeScript, GraphQL Modules and Apollo-Server. If you wanted to create a GraphQL API in Java, you have that option as well using Springboot/Java GraphQL.

As we started to develop GraphQL schemas, resolver and datasources, we considered how to setup the directory structure in such away to allow us to modularize our app. That is where GraphQL Modules comes in.

GraphQL Modules is a toolset of libraries and guidelines dedicated to create reusable, maintainable, testable and extendable modules out of your GraphQL server.

Your GraphQL server uses a schema to describe the shape of your available data. This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute.

export const TvShow = gql`

  type Query {
    show(name: String!): TvShow
  }  

  type TvShow {
    id: Int
    url: String!
    name: String!
    type: String
    language: String
    genres: [String]
    status: String
    runtime: Int
    averageRuntime: Int
    premiered: String
    ended: String
    officialSite: String
    schedule: Schedule
    rating: Rating
    weight: Int

  }

  type Schedule{
    time: String
    days: [String]
  }
  
  type Rating {
    average: Float
  }

  type Network {
    id: ID!
    name: String
    country: Country
  }
`;

The TvShow Schema consist of types and queries. The query defined here is named “show” and it takes a parameter of type String and it returns a TvShow object.

The ! exclamation mark stands for non-nullable, meaning that the GraphQL service promises to always give you a value when you query this field.

In this implementation, we use GET query to retrieve data. However if you want to do updates, then you need to create a Mutation.

Data sources are classes that Apollo Server can use to encapsulate fetching data from a particular source, such as a database or a REST API. These classes help handle caching, deduplication, and errors while resolving operations.

Your server can use any number of different data sources. You don’t have to use data sources to fetch data, but they’re strongly recommended.

import { HTTPCache, RESTDataSource } from 'apollo-datasource-rest';
import { Injectable } from 'graphql-modules';

@Injectable()
export class TvMazeAPI extends RESTDataSource {
  
  constructor() {

    //Get an error "message": "Cannot read property 'fetch' of undefined",
    //because this.httpCache is null
    super();
    this.httpCache = new HTTPCache(); 
    this.baseURL = 'https://api.tvmaze.com/';
  }

  async getByTvShowName(showName: string): Promise<string> {
    const response = await this.get('singlesearch/shows', { q: showName });
    return response;
  }


  async getCrewInformationByTvShowId(showId: number): Promise<string> {
    const response = await this.get(`shows/${showId}/crew`);
    return response;
  }

}

The TvMazeAPI Provider extends RESTDataSource. This means the data will be retrieved and/or manipulated using REST.

The Public API GET endpoints which are implemented in TvMazeAPI Data Source are:

Having @Injectable()decorator on top of TvMazeAPI class allows it to be accessed through Dependency Injection.

A resolver is a function that’s responsible for populating the data for a single field in your schema. Whenever a client queries for a particular field, the resolver for that field fetches the requested data from the appropriate data source.

import { TvMazeAPI } from '../../providers/tvmazeapi.provider';


export const TvShowResolver = {
  Query: {
    show: (root, args, context, info) => {
      return context.injector.get(TvMazeAPI).getByTvShowName(args.name);
    }
  },
};

The TvShowResolver is the middle layer, which maps the REST JSON response we receive from the API (TvMazeAPI) to return a TvShow.

It provides implementations for the queries we define in our schemas. We are here stating that if Query (show) is called, then call the method getByTvShowName(arguments.name) which exists in TvMazeAPI.

Through dependency injection, we are able to retrieve an instance of TvMazeAPI in our resolver.

In GraphQL, a context is an object shared by all the resolvers of a specific execution. It’s useful for keeping data such as authentication info, the current user, database connection, data sources and other things you need for running your business logic.

GraphQL Modules follow the same approach, so context is shared across modules. That’s why there’s no API for context building in GraphQL Modules, it’s managed by GraphQL server implementation.

https://www.graphql-modules.com/docs/essentials/context/

Injector is responsible for registering and managing Services and Injection Tokens (and their values). Basically managing their own space. Every Module has its own Injector that has one parent which is Injector of the application.

https://www.graphql-modules.com/docs/di/introduction

Create a GraphQL Module:

Creating a GraphQL Module requires that you define a Schema-typeDef(s), Resolver(s) and Provider(s).

import{ TvShow } from './tvshow.schema';
import { TvShowResolver } from './tvshow.resolver';
import { createModule } from 'graphql-modules';
import { TvMazeAPI } from '../../providers/tvmazeapi.provider';
import { CommonSchemas } from '../common-schemas/common.schema';

export const TvShowModule = createModule({
  id: 'tvshow-module',
  typeDefs: [CommonSchemas, TvShow],
  resolvers: [TvShowResolver],
  providers: [TvMazeAPI]
});

Once you have your Module created, we will then need to create the Apollo-Server application, and provide all application modules to it.

import "reflect-metadata";

import { TvShowModule } from './modules/tvshow';
import { TvShowCrewModule } from "./modules/tvshow_crew";
import { createApplication } from 'graphql-modules';

//We will merge all our modules.
export const application = createApplication({
  modules: [TvShowModule, TvShowCrewModule],
});


Using Apollo-SerVer with GraphQL Modules:

To use GraphQL Modules with Apollo-Server, you can use createSchemaForApollo to get a schema that is adapted for this server. GraphQL Modules integrates with other servers such as Express GraphQL, GraphQL-Helix, and others.

import "reflect-metadata";

import { ApolloServer } from 'apollo-server';
import { application } from './app';


//As we could have more than 1 schema, we need to merge them.
const schema = application.createSchemaForApollo();

const server = new ApolloServer({
    schema,
});

server.listen().then(() => {
    console.log(`
      Server is running!
      Listening on port 4000
      Explore at https://studio.apollographql.com/sandbox
    `);
});
Using Apollo-Server Sandbox:

Once your application is up and running, you could access Apollo-Server through the Sandbox URL.

To verify a Query in Apollo-Server, you will need to provide the query “name”, and the parameters required.

For the TvShow example, our query is “show”, the parameter passed is a JSON { “name”, “girls” }, and we specified what attributes of a TvShow we want to be returned as part of our response.

The types defined here are id, url, name, type, language, genres, etc.


Integration Testing GraphQL Modules:

GraphQL Modules provides a set of utilities for testing your modules and also for more granular testing of module’s smaller units, like providers and middlewares.

To access the testing utilities, import testkit object from graphql-modules package.

For our integration test, we reused our application logic, and schemas, but replaced the provider method “getByTvShowName” to return our mocked results by using testkit replaceModule function.

We then execute the final graphQL query to validate it against the mocked response using testkit.execute.

import 'reflect-metadata';
import { TvShowModule } from '..';
import { gql, testkit, } from 'graphql-modules';
import { TvMazeAPI } from '../../../providers/tvmazeapi.provider';
import { application } from '../../../app';

describe("Validate TvShowModule", () => {

  it('Validate Application Setup', () => {
    const app = testkit.testModule(TvShowModule, {
      replaceExtensions: true,
    });
    expect(app.schema.getQueryType())
      .toBeDefined();
  });

  describe('Query show(name) from RestDataSource', () => {

    it('Validate TvMazeAPI.getByTvShowName(showName) to return TvShow', async () => {

      //We will mock the Module by intercepting the getByTvShowName method call.  
      //The Schema stays the same
      const app = testkit.mockApplication(application)
        .replaceModule(testkit.mockModule(TvShowModule, {
          providers: [
            {
              provide: TvMazeAPI,
              useValue: {
                getByTvShowName(showName: string) {
                  return (
                    { "id": 139, "url": "https://www.tvmaze.com/shows/139/girls", "name": `${showName}`, "type": "Scripted", "language": "English", "genres": ["Drama", "Romance"], "status": "Ended", "runtime": 30, "averageRuntime": 30, "premiered": "2012-04-15", "ended": "2017-04-16", "officialSite": "http://www.hbo.com/girls", "schedule": { "time": "22:00", "days": ["Sunday"] }, "rating": { "average": 6.6 }, "weight": 97, "network": { "id": 8, "name": "HBO", "country": { "name": "United States", "code": "US", "timezone": "America/New_York" } }, "webChannel": null, "dvdCountry": null, "externals": { "tvrage": 30124, "thetvdb": 220411, "imdb": "tt1723816" }, "image": { "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/31/78286.jpg", "original": "https://static.tvmaze.com/uploads/images/original_untouched/31/78286.jpg" }, "summary": "<p>This Emmy winning series is a comic look at the assorted humiliations and rare triumphs of a group of girls in their 20s.</p>", "updated": 1611310521, "_links": { "self": { "href": "https://api.tvmaze.com/shows/139" }, "previousepisode": { "href": "https://api.tvmaze.com/episodes/1079686" } } }
                  )
                },
              },
            },
          ],
        }));

      const promiseResult = testkit.execute(app, {
        document: gql`query Query($name: String!) {
          show(name: $name) {
            id
            url
            name
            type
            language
            genres
            status
            runtime
            averageRuntime
            premiered
            ended
            officialSite
            schedule {
              time
              days
            }
            rating {
              average
            }
            weight
          }
        }`,
        variableValues: { name: "girls" },
      });

      const result = (await promiseResult).data;

      //Returns a TvShow
      expect(result)
        .toHaveProperty('show');

      expect(result?.show)
        .toHaveProperty('id', 139);

      expect(result?.show)
        .toHaveProperty('name', "girls");


      //Expect Exact JSON response
      const expectedJSONResponse = {
        "id": 139,
        "url": "https://www.tvmaze.com/shows/139/girls",
        "name": "girls",
        "type": "Scripted",
        "language": "English",
        "genres": [
          "Drama",
          "Romance"
        ],
        "status": "Ended",
        "runtime": 30,
        "averageRuntime": 30,
        "premiered": "2012-04-15",
        "ended": "2017-04-16",
        "officialSite": "http://www.hbo.com/girls",
        "schedule": {
          "time": "22:00",
          "days": [
            "Sunday"
          ]
        },
        "rating": {
          "average": 6.6
        },
        "weight": 97
      };

      expect(result?.show).toEqual(expectedJSONResponse);

    });

    it('Validate TvMazeAPI.getByTvShowName(showName) await TypeError', async () => {

      const app = testkit.mockApplication(application)
        .replaceModule(testkit.mockModule(TvShowModule, {
          providers: [
            {
              provide: TvMazeAPI,
              useValue: {
                getByTvShowName(showName: string) {
                  throw TypeError();
                },
              },
            },
          ],
        }));

      const promiseResult = testkit.execute(app, {
        document: gql`query Query($name: String!) {
                show(name: $name) {
                  id,
                  name
                }
              }`,
        variableValues: { name: "girls" },
      });

      const result = (await promiseResult).data;

      expect(result)
        .toHaveProperty('show');

      expect(result?.show)
        .toBeNull();

    });

  });

});


Github: https://github.com/smustafa/apollo-graphql-modules-example

Leave a comment