Godwin Ekuma I learn so that I can solve problems.

GraphQL Modules tutorial: How to modularize GraphQL schema

6 min read 1898

graphql modules schema modularization

In this tutorial, we’ll show you how to use GraphQL Modules to break your GraphQL application into simple, reusable, feature-based modules.

We’ll cover the following:

What are GraphQL Modules?

GraphQL Modules is a set of libraries and guidelines designed to help you create reusable, maintainable, testable, and extendable modules. The basic concept behind GraphQL Modules is to separate your GraphQL server into smaller, independent, feature-based modules.

GraphQL Modules help you implement the separation of concerns design pattern in GraphQL, enabling you to write simple modules that only do what they need to. This makes your code easier to write, test, and maintain.

The structure of an initial, basic implementation of a GraphQL server would look something like this:

GraphQL Server Basic Implementation Example
Basic GraphQL implementation

As your GraphQL application grows, so will the complexity of your schema and resolvers, which can make schema maintenance difficult. To organize your code, you’ll want to separate your schema types and their associated resolvers into multiple files or modules and add anything related to a specific part of the app under a module. The graphql-modules library helps facilitate this gradual process.

With graphql-modules, your app would be structured as follows:

GraphQL-Modules Library App Structure

Why use GraphQL Modules?

Most developers who use GraphQL Modules select the toolset for its reusable modules, scalable structure, clear path to growth, and testability. Let’s take a closer look at some of the reasons why developers turn to GraphQL Modules to write reusable, extendable modules for their GraphQL apps.

  • Reusable modules: Modules are defined by their GraphQL schema in smaller pieces, which you can later move and reuse
  • Scalable structure: Each module has a clearly defined boundary, which makes managing multiple teams and features, multiple microservices, and servers a lot easier. Modules can communicate between each other using custom messages
  • Clear path to growth: By separating features into modules, it’s easy to see how your application can grow from a very simple, fast, single-file modules to scalable, multi-file, -team, -repo and -server modules
  • Testability: Testing small pieces of code is easier than testing larger chunks of code. GraphQL Modules provides a rich toolset for testing and mocking your application

How GraphQL Modules work

We’ll use a simple library application to show how to break your GraphQL application into modules. The application will have Book, Author, and Genre modules and each module will be composed of type definitions and resolver functions.

Creating a GraphQL module

To start using graphql-modules, all you need is to install its package and graphql:

// NPM
npm install --save graphql graphql-modules

//Yarn 

yarn add graphql graphql-modules

To create a module, use createModule:

import { createModule } from 'graphql-modules';

export const myModule = createModule({
  id: 'my-module',
  dirname: __dirname,
  typeDefs: [
    `type Query {
      hello: String!
    }`,
  ],
  resolvers: {
    Query: {
      hello: () => 'world',
    },
  },
});

Adding a unique id to the module is important because it will help you locate issues in your type definition. The dirname, though optional, makes it simpler to match the correct file when an exception occurs.

Type definitions and resolvers

For defining schemas, GraphQL Modules uses Schema Definition Language (SDL), just like GraphQL Schema.

GraphQL Modules also implements resolvers in the same manner as any other GraphQL implementation. According to the GraphQL Modules documentation, modules created by GraphQL Modules can detect duplicate, incorrect, or old resolvers (e.g., resolvers that don’t match type definitions or extensions).

Creating the book module

Here’s how to create a book module for our GraphQL app:

// book.type.graphql
import { gql } from 'graphql-modules';

export const Book = gql`
  type Query {
      book(id: ID!): Book
  }
  type Book {
    id: String
    title: String
    author: Author
    summary: String
    isbn: String
    genre: [Genre]
    url: String
  }
`;


// book.resolver.graphql

export const BookResolver = {
    Query: {
      book(root, { id }) {
        return {
          _id: id,
          title: "To The Lighthouse",
          author: "Virginia Woolf",
          summary:"Book summary",
          isbn: "12345678EDB"
          genre: ["ficton"],
          url: "http://lighouse.com"
        };
      },
    },
    Book: {
      id(book) {
        return book._id;
      },
      title(book) {
        return book.title;
      },
      author(book) {
        return book.author;
      },
      summary(book) {
        return book.summary;
      },
      isbn(book) {
        return book.isbn;
      },
      genre(book) {
        return book.genre;
      },
      url(book) {
        return book.url;
      },
    },
  },



// book.module.graphql.ts
import{ Book } from './book.type.graphql';
import { BookResolver } '.book.resolver.graphql';
import { createModule } from 'graphql-modules';

export const BookModule = createModule({
  id: 'book-module',
  dirname: __dirname,
  typeDefs: [Book],
  resolvers: [BookResolvers]
});

Other modules

To create the author and genre modules for our example GraphQL library app, follow the steps below:

// author.module.graphql.ts
import{ Author } from './author.type.graphql';
import { AuthorResolver } '.author.resolver.graphql';
import { createModule } from 'graphql-modules';

export const AuthorModule = createModule({
  id: 'book-module',
  dirname: __dirname,
  typeDefs: [Author],
  resolvers: [AuthorResolvers]
});


// genre.module.graphql.ts
import{ Genre } from './genre.type.graphql';
import { GenreResolver } '.genre.resolver.graphql';
import { createModule } from 'graphql-modules';

export const GenreModule = createModule({
  id: 'book-module',
  dirname: __dirname,
  typeDefs: [Genre],
  resolvers: [GenreResolvers]
});

Merging schemas with GraphQL Modules

Each of the modules we defined contributes to a small part of the entire schema.

To merge the schemas together, create an application using GraphQL Modules’ createApplication:

import { createApplication } from 'graphql-modules';
import { GenreModule } from './genre/genre.module.graphql';
import { BookModule } from './book/book.module.graphql';
import { AuthorModule } from './author/author.module.graphql';

export const application = createApplication({
  modules: [BookModule, AuthorModule, GenreModule],
});

The application contains your GraphQL schema and the implementation of it. We need to make it consumable by a GraphQL server.

GraphQL Modules have various implementations for popular GraphQL servers, such as Apollo, Express GraphQL, and GraphQL Helix. If you’re using Apollo Server, you can use createSchemaForApollo to get a schema that is adapted for this server and integrates with it perfectly:

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

const schema = application.createSchemaForApollo();

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

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

GraphQL context

In GraphQL, the contextargument is shared across all resolvers that are executing for a particular operation. You can use context to share per-operation state, including authentication data, dataloader instances, etc.

In GraphQL Modules, context is available as the third argument to each resolver and is shared across modules:

const resolvers = {
  Query: {
    myQuery(root, args, context, info) {
      // ...
    },
  },
};

Dependency injection

As your application expands, you may need to separate certain business logic from the resolver into a service. GraphQL Modules supports dependency injection (DI), which helps you make your services available to the resolvers.

Dependencies are services or objects that a resolvers needs to perform its function. So rather creating a service, a resolver requests it from an external resource.



It’s important to note that dependency injection makes sense only when your codebase is quite large and you need to move fast.

To use GraphQL Modules’ DI, there are few terms you should understand:

  • injector is an object in in the DI system that can find a named dependency in its cache or create a dependency using a configured provider and then make it available to a module or the entire application depending on its scope
  • InjectionToken is a symbol (token) or class (service class) that represents an object or any value in the dependency injection space
  • provider is a way to define a value and match it with a Token or a Service class. It provides the value of a specific injection token. Providers are injected into resolvers or other services.

There are three kinds of providers: class providers, value providers, and factory providers. Let’s take a closer look at each.

Class provider

A class provider creates an instance of a class and makes it available to the injector. For a service class to be used as a provider, it has to be decorated with the @Injectable() decorator:

// book.service.ts
import { Injectable } from 'graphql-modules';

@Injectable()
export class BookService {}


// book.module.graphql.ts
import{ Book } from './book.type.graphql';
import { BookResolver } '.book.resolver.graphql';
import { createModule } from 'graphql-modules';

export const BookModule = createModule({
  id: 'book-module',
  dirname: __dirname,
  typeDefs: [Book],
  resolvers: [BookResolvers],
  providers: [BookService]
});

The context object is available in every resolver. This context object contains another object, injector, which can be used to access a provided service class in a resolver.

// book.resolver.graphql
import { BookService } from './book.service.ts'
export const BookResolver = {
    Query: {
      book(root, { id }, context) {
        const bookService = context.injector.get(BookService);
        return {
          _id: id,
         // ...
        };
      },
    },
    Book: {
      id(book) {
        return book._id;
      },
      // ...
    },
  },

To use it in a class, simply ask for it in the constructor.

import { Injectable } from 'graphql-modules';
import { BookService } from './book';

@Injectable()
class AuthorService {
  constructor(private bookService: BookService) {}

  allbooks(authorId) {
     return this.bookService.books(authorId)
  }
}

Value provider

The value provider provides a ready-to-use value. It requires a token that represents a value, either InjectionToken or a class.

import { InjectionToken } from 'graphql-modules';
const ApiKey = new InjectionToken<string>('api-key');
import { createModule } from 'graphql-modules';

export const BookModule = createModule({
  id: 'book-module',
  /* ... */
  providers: [
    {
      provide: ApiKey,
      useValue: 'my-api-key',
    },
  ],
});

Factory provider

The factory provider is a function that provides a value. It comes in handy when you need to create a dependent value dynamically based on information that would not be available until run time. You can make an informed decision on which value to return based on certain conditions of the application state.

The factory provider is also useful for creating an instance of a class, such as when using third-party libraries. The factory function can have an optional argument with dependencies, if any.

import { InjectionToken } from 'graphql-modules';
const ApiKey = new InjectionToken<string>('api-key');
import { createModule } from 'graphql-modules';

export const BookModule = createModule({
  id: 'book-module',
  /* ... */
  providers: [
    {
      provide: ApiKey,
      useFactory(config: Config) {
        if (context.environment) {
          return 'my-api-key';
        }

        return null;
      },
      deps: [Config],
    },
  ],
});

Accessing a token in a resolver is similar to accessing a service. The @Inject decorator is used to access a token in a constructor.


More great articles from LogRocket:


import { Injectable } from 'graphql-modules';
import { BookService } from './book';

@Injectable()
class AuthorService {
  constructor(@Inject(ApiKey) private key: string, private bookService: BookService) {}

  allbooks(authorId) {
     return this.bookService.books(authorId)
  }
}

GraphQL scopes

Each token or provider has a scope that is used to define its lifecycle. Scope can either be a singleton or an operation. Every provider or token defined as a singleton is created once and the same instance is available for all incoming GraphQL operations. A singleton provider lives throughout the lifecycle of the application and is the default and recommended scope for GraphQL Modules.

Operation providers, on the other hand, are created per execution context or for each GraphQL operation that requires it. An operation provider lives only the lifecycle of the GraphQL operation that instantiated it.

// genre.service.ts
import { Injectable, Scope } from 'graphql-modules';

@Injectable({
  scope: Scope.Operation,
})
export class GenreService {}


// genre.module.graphql.ts
import{ Genre } from './genre.type.graphql';
import { GenreResolver } '.genre.resolver.graphql';
import { createModule } from 'graphql-modules';
import { GenreService } from './genre.service.ts'

export const GenreModule = createModule({
  id: 'book-module',
  dirname: __dirname,
  typeDefs: [Genre],
  resolvers: [GenreResolvers],
  providers: [GenreService]
});

When GenreService is not called by resolvers, the service is not created.

Conclusion

Maintainable code is easily scalable, and GraphQL Modules aim to help you achieve just that.

It’s worth nothing that code separation or modularization only happens during development. However, at runtime, a unified schema is still served.

You should now have everything you need to get started using GraphQL Modules. To learn about other advanced concepts, such as subscription, middleware, execution context, and lifecycle hooks, you can check out GraphQL Modules documentation.

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 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. .
Godwin Ekuma I learn so that I can solve problems.

One Reply to “GraphQL Modules tutorial: How to modularize GraphQL schema”

  1. Thank you, for your article. Have one question.How can we declare custom directives and scalars with this new version of graphql-modules?

Leave a Reply