Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
23 Jul 2025 · 6 min read ·Article 23 / 125
Go

23 Modularizing Resolver and Schema Files

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

23 Modularizing Resolver and Schema Files: A Structured Approach to Building a GraphQL API

Building a GraphQL API often leads us into modularity problems with our schema and resolvers. If left to grow monolithically, schemas and resolvers easily become hard to read, prone to import conflicts, and a major liability when another engineer has to maintain them — or even when we ourselves have to, somewhere down the line.

In this article, I’ll walk through techniques for modularizing resolver and schema files in a Node.js GraphQL project using apollo-server, and illustrate best practices with code examples, scenario simulations, and even a modularization diagram drawn with mermaid. All of it in a simple yet professional style, the way we engineers share insights with one another on Medium.


Why Do We Need Modularization?

Let’s start with the common problem. Typically, in the early stages, we define the entire schema and all resolvers in a single file — say schema.js and resolvers.js — like this:

js
 1// schema.js
 2const { gql } = require('apollo-server');
 3
 4const typeDefs = gql`
 5  type Query {
 6    books: [Book]
 7  }
 8  type Book {
 9    title: String
10    author: String
11  }
12`;
13
14module.exports = typeDefs;
15
16// resolvers.js
17const resolvers = {
18  Query: {
19    books: () => [{ title: "1984", author: "Orwell" }]
20  }
21};
22
23module.exports = resolvers;

This is fine for a prototype or a very small scale. But as resources grow (for example, new entities such as User, Post, Comment), those files keep ballooning. Everything piles up in a single file, collaboration becomes difficult, testing becomes a hassle, and the risk of merge conflicts runs high.

Summary of the Challenges Without Modularization

ProblemNegative Effect
Files pile upHard to navigate & refactor
Naming conflictErrors go undetected until runtime
Hard to testTest setup & teardown become complicated
Merge conflictFrequent collisions during collaboration

Principles of Modularizing Resolvers & Schemas

Goal: Each entity (domain) has its own schema and resolver file, which are then merged automatically. It’s similar to the “feature folder” concept on the frontend (in React, for example).

For instance, we have a folder structure like this:

text
 1src/
 2 ├── graphql/
 3 |    ├── book/
 4 |    |    ├── typeDefs.js
 5 |    |    └── resolvers.js
 6 |    ├── user/
 7 |    |    ├── typeDefs.js
 8 |    |    └── resolvers.js
 9 |    └── index.js
10 └── server.js

Benefits:

  • Easy to scale features up or down
  • Other teams can collaborate without touching the main file
  • Test per domain/entity
  • Maintainable & readable

Implementing Modular GraphQL

Let’s go through, step by step, how to modularize the schema and resolver files.

1. Define the Schema and Resolver per Domain

1.1. Book Module

js
 1// src/graphql/book/typeDefs.js
 2const { gql } = require('apollo-server');
 3
 4const bookTypeDefs = gql`
 5  type Book {
 6    id: ID!
 7    title: String!
 8    author: String!
 9  }
10  extend type Query {
11    books: [Book!]!
12    bookById(id: ID!): Book
13  }
14`;
15
16module.exports = bookTypeDefs;
17
18// src/graphql/book/resolvers.js
19const bookResolvers = {
20  Query: {
21    books: () => [...], // can be filled with dummy/mock data
22    bookById: (_, { id }) => {...}
23  }
24};
25
26module.exports = bookResolvers;

1.2. User Module

js
 1// src/graphql/user/typeDefs.js
 2const { gql } = require('apollo-server');
 3
 4const userTypeDefs = gql`
 5  type User {
 6    id: ID!
 7    name: String!
 8    email: String!
 9  }
10  extend type Query {
11    users: [User!]!
12    userById(id: ID!): User
13  }
14`;
15
16module.exports = userTypeDefs;
17
18// src/graphql/user/resolvers.js
19const userResolvers = {
20  Query: {
21    users: () => [...],
22    userById: (_, { id }) => {...}
23  }
24};
25
26module.exports = userResolvers;

2. Root Schema (“Stitching”)

We need a single “root” schema and resolver, usually in graphql/index.js:

js
 1// src/graphql/index.js
 2const { gql } = require('apollo-server');
 3const { mergeTypeDefs, mergeResolvers } = require('@graphql-tools/merge');
 4
 5const bookTypeDefs = require('./book/typeDefs');
 6const userTypeDefs = require('./user/typeDefs');
 7const bookResolvers = require('./book/resolvers');
 8const userResolvers = require('./user/resolvers');
 9
10// Root type Query (needed when using extend type in modules)
11const rootTypeDefs = gql`
12  type Query
13`;
14
15const typeDefs = mergeTypeDefs([
16  rootTypeDefs, bookTypeDefs, userTypeDefs
17]);
18const resolvers = mergeResolvers([
19  bookResolvers, userResolvers
20]);
21
22module.exports = { typeDefs, resolvers };
Danger
Note: The @graphql-tools/merge package is a huge help for stitching schemas/resolvers together cleanly.

3. Setting Up the Server

js
 1// src/server.js
 2const { ApolloServer } = require('apollo-server');
 3const { typeDefs, resolvers } = require('./graphql');
 4
 5const server = new ApolloServer({
 6  typeDefs,
 7  resolvers
 8});
 9
10server.listen().then(({ url }) => {
11  console.log(`🚀  Server ready at ${url}`);
12});

Simulating a Scenario for Adding a Module

What if the product owner asks to add a new feature, say “Comments”?
Just add a new folder:

text
1src/graphql/comment/
2 ├── typeDefs.js
3 └── resolvers.js

Then import it into src/graphql/index.js:

js
 1// ...existing code ...
 2const commentTypeDefs = require('./comment/typeDefs');
 3const commentResolvers = require('./comment/resolvers');
 4
 5const typeDefs = mergeTypeDefs([
 6  rootTypeDefs, bookTypeDefs, userTypeDefs, commentTypeDefs
 7]);
 8const resolvers = mergeResolvers([
 9  bookResolvers, userResolvers, commentResolvers
10]);

Done! Without fiddling with any other file.


Illustrating the Modular Initiation Flow

Let’s visualize this modular architecture with a mermaid diagram.

MERMAID
graph TD
    A[Server.js] --> B[graphql/index.js]
    B --> C[book/typeDefs.js & resolvers.js]
    B --> D[user/typeDefs.js & resolvers.js]
    B --> E[comment/typeDefs.js & resolvers.js]

Comparison Table: Before vs. After Modularization

AspectMonolithicModular
Adding featuresHard, conflict-proneVery easy
TeamworkFrequent merge crashesSeparate files, safe
TestingE2E-dominatedCan unit test per module
MaintainabilityDegrades over timeMinimal changes
ScalabilityWeakEasy to grow

Tips for Modularizing Resolvers & Schemas

  1. Use a folder per domain
    • Easier to track, scale, and debug issues.
  2. Provide a root folder for glue/stitching
    • For example, /graphql/index.js.
  3. Don’t forget the root type Query
    • If every typeDefs uses extend type Query, you need a root type Query for the initial schema.
  4. Separate schema and resolver
    • More flexible to test and refactor.
  5. Use merge tools
    • Don’t hardcode schema/resolver integration — use a helper library.
  6. Add tests per module
    • Unit test each resolver & schema.

Closing

By modularizing our schemas and resolvers, our GraphQL API becomes far more maintainable, scalable, and pleasant to collaborate on. This technique isn’t just a trendy “modern” best practice — it has long proven to be a fundamental in many large-scale production GraphQL codebases.

With the code examples, diagram, and case simulation above, I hope you feel even more confident about building a GraphQL API that’s ready to scale up professionally. Share this article if you find it useful — and don’t hesitate to discuss it in the comments, because who knows, we might just learn from each other for the next round of modularization!

Happy coding, and stay modular! 🚀


Resources:

Related Articles

💬 Comments