23 Modularizing Resolver and Schema Files
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:
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
| Problem | Negative Effect |
|---|---|
| Files pile up | Hard to navigate & refactor |
| Naming conflict | Errors go undetected until runtime |
| Hard to test | Test setup & teardown become complicated |
| Merge conflict | Frequent 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:
1src/
2 ├── graphql/
3 | ├── book/
4 | | ├── typeDefs.js
5 | | └── resolvers.js
6 | ├── user/
7 | | ├── typeDefs.js
8 | | └── resolvers.js
9 | └── index.js
10 └── server.jsBenefits:
- 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
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
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:
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 };@graphql-tools/merge package is a huge help for stitching schemas/resolvers together cleanly.3. Setting Up the Server
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:
1src/graphql/comment/
2 ├── typeDefs.js
3 └── resolvers.jsThen import it into src/graphql/index.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.
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
| Aspect | Monolithic | Modular |
|---|---|---|
| Adding features | Hard, conflict-prone | Very easy |
| Teamwork | Frequent merge crashes | Separate files, safe |
| Testing | E2E-dominated | Can unit test per module |
| Maintainability | Degrades over time | Minimal changes |
| Scalability | Weak | Easy to grow |
Tips for Modularizing Resolvers & Schemas
- Use a folder per domain
- Easier to track, scale, and debug issues.
- Provide a root folder for glue/stitching
- For example,
/graphql/index.js.
- For example,
- Don’t forget the root
type Query- If every typeDefs uses
extend type Query, you need a roottype Queryfor the initial schema.
- If every typeDefs uses
- Separate schema and resolver
- More flexible to test and refactor.
- Use merge tools
- Don’t hardcode schema/resolver integration — use a helper library.
- 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: