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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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}`);
});
contextIn 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) {
// ...
},
},
};
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.
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.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly aggregating and reporting 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.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
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 now
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?