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:
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:
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:
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.
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.
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.
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).
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] });
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] });
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}`); });
context
In GraphQL, the context
argument 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) { // ... }, }, };
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 scopeInjectionToken
is a symbol (token) or class (service class) that represents an object or any value in the dependency injection spaceprovider
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.
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) } }
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', }, ], });
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.
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) } }
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.
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.
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.Would you be interested in joining LogRocket's developer community?
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 nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
One Reply to "GraphQL Modules tutorial: How to modularize GraphQL schema"
Thank you, for your article. Have one question.How can we declare custom directives and scalars with this new version of graphql-modules?