Introduction
This is part 2 of our tutorial series. In the previous tutorial we setup our project and using graphql-yoga created a Todos GraphQL server covering all the CRUD operations. In this tutorial we will be using graphql-modules and modularize our codebase.
Overview
I've seen code bases where people club all the controllers under controllers folder, routes under routes, models under models folder. Similarly, for GraphQL projects they club all resolvers under resolvers and schemas under schema folder. I like to club my files by features, so I will create a todos folder, it will contain todos.controller.ts, todos.model.ts, todos.router.ts, etc. In this tutorial we will club our files by features. 
This series is not recommended for beginners some familiarity and experience working with Nodejs, GraphQL & Typescript is expected. In this tutorial we  will cover the following : -
- Setup the folder structure.
 - Create Todos schema and resolvers.
 - Create Comments schema and resolvers.
 - Run the Server and Test it.
 
All the code for this tutorial is available under the graphql-modules branch, check the repo.
Step One: Folder Setup
First, we will create a modules folder and inside the modules folder create 2 more folders todos & comments. Inside each of these folders create resolver.ts, a schmea.graphql and an entry index.ts file. We are basically grouping files by features - 
From your terminal install the following dependencies -
yarn add graphql-modules graphql-import-node @envelop/graphql-modules 
- 
graphql-modules, will help us in modularizing our codebase and separate our GraphQL schema into manageable small bits & pieces. - We will be using 
.graphqlextension for schema files. To work with.graphqlfiles in a Nodejs project we usegraphql-import-nodepackage. - Finally, to make our modules work with 
graphql-yogawe use the@envelop/graphql-modulespackage. 
Step Two: Create Todos Schema & Resolvers
Now we will move the schema and resolvers that we created for todos & comments in the previous tutorial to their respective folders. First inside modules/todos/schema.graphql paste the schema -
enum TaskStatus {
    PENDING
    IN_PROGRESS
    DONE
}
type Todo {
    id: String!
    task: String!
    description: String!
    status: TaskStatus!
    tags: [String]!
    comments: [Comment]
    createdAt: String!
    updatedAt: String!
}
type Query {
    todos: [Todo]!
}
input TodoInput {
    task: String!
    description: String!
    status: TaskStatus!
    tags: [String]
}
input EditTodoInput {
    id: ID!
    task: String!
    description: String!
    status: TaskStatus!
    tags: [String]
}
input DeleteTodoInput {
    id: ID!
}
type Mutation {
    addTodo(input: TodoInput): Todo!
    editTodo(input: EditTodoInput): Todo!
    deleteTodo(input: DeleteTodoInput): Todo!
}
Notice that all the types, inputs, queries & mutations related to the Todos type have been moved here. Similarly lets move our resolvers for Todos to modules/todos/resolvers.ts -
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";
import { Todos } from "@prisma/client";
import { GraphQLContext } from "../../context";
export const todosResolvers = {
  Query: {
    todos(parent: unknown, args: {}, context: GraphQLContext) {
      return context.prisma.todos.findMany();
    },
  },
  Todo: {
    comments(parent: Todos, args: {}, context: GraphQLContext) {
      return context.prisma.comments.findMany({
        where: {
          todoId: parent.id,
        },
      });
    },
  },
  Mutation: {
    addTodo(parent: unknown, args: { input: Todos }, context: GraphQLContext) {
      return context.prisma.todos.create({
        data: {
          task: args.input.task,
          description: args.input.description,
          status: args.input.status,
          tags: args.input.tags,
        },
      });
    },
    async editTodo(
      parent: unknown,
      args: { input: Todos },
      context: GraphQLContext
    ) {
      const todoId = args.input.id;
      try {
        const todo = await context.prisma.todos.update({
          data: {
            task: args.input.task,
            description: args.input.description,
            status: args.input.status,
            tags: args.input.tags,
          },
          where: {
            id: todoId,
          },
        });
        return todo;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code == "P2003"
        ) {
          return Promise.reject(
            new GraphQLError(
              `Cannot delete a non-exiting todo with id ${todoId}`
            )
          );
        }
        return Promise.reject(error);
      }
    },
    async deleteTodo(
      parent: unknown,
      args: { input: Pick<Todos, "id"> },
      context: GraphQLContext
    ) {
      const todoId = args.input.id;
      try {
        const todo = await context.prisma.todos.delete({
          where: {
            id: todoId,
          },
        });
        return todo;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code == "P2003"
        ) {
          return Promise.reject(
            new GraphQLError(
              `Cannot delete a non-exiting todo with id ${todoId}`
            )
          );
        }
        return Promise.reject(error);
      }
    },
  },
};
Finally under modules/todos/index.ts file we create our module -
import { createModule } from "graphql-modules";
import * as TodosSchema from "./schema.graphql";
import { todosResolvers } from "./resolvers";
export const todosModule = createModule({
  id: "todos-module",
  dirname: __dirname,
  typeDefs: [TodosSchema],
  resolvers: todosResolvers,
});
You might be getting a type error for the ./schema.graphql import, to resolve this add the following import at the top of src/index.ts -
import "graphql-import-node";
Step Three: Create Comments Schema & Resolvers
Lets repeat Step Two, this time for the comments module, under modules/commeants/schema.graphql paste the following -
type Comment {
  id: ID!
  body: String!
}
type Query {
  comment(id: ID!): Comment!
}
input CommentInput {
  todoId: ID!
  body: String!
}
type Mutation {
  postCommentOnTodo(input: CommentInput): Comment!
}
Under modules/comments/resolvers.ts paste -
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";
import { GraphQLContext } from "../../context";
export const commentsResolvers = {
  Query: {
    comment(parent: unknown, args: { id: string }, context: GraphQLContext) {
      return context.prisma.comments.findUnique({
        where: {
          id: args.id,
        },
      });
    },
  },
  Mutation: {
    async postCommentOnTodo(
      parent: unknown,
      args: { input: { todoId: string; body: string } },
      context: GraphQLContext
    ) {
      const todoId = args.input.todoId;
      try {
        const comment = await context.prisma.comments.create({
          data: {
            todoId,
            body: args.input.body,
          },
        });
        return comment;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code == "P2003"
        ) {
          return Promise.reject(
            new GraphQLError(
              `Cannot comment on a non-exiting todo with id ${todoId}`
            )
          );
        }
        return Promise.reject(error);
      }
    },
  },
};
Finally, under modules/comments/index.ts create the comments module -
import { createModule } from "graphql-modules";
import * as CommentsSchema from "./schema.graphql";
import { commentsResolvers } from "./resolvers";
export const commentsModule = createModule({
  id: "comments-module",
  dirname: __dirname,
  typeDefs: [CommentsSchema],
  resolvers: commentsResolvers,
});
Step Four: Run the Server & Test it
Now that we have created our modules and modularized our codebase, lets stitch it with graphql-yoga build it and test it. Under modules/index.ts lets create an application from all our modules -
import { createApplication } from "graphql-modules";
import { todosModule } from "./todos";
import { commentsModule } from "./comments";
export const application = createApplication({
  modules: [todosModule, commentsModule],
});
Now in the src/index.ts file lets import this application and stitch it with graphql-yoga -
import "graphql-import-node";
import { createServer } from "http";
import { createYoga } from "graphql-yoga";
import { useGraphQLModules } from "@envelop/graphql-modules";
import { createContext } from "./context";
import { application } from "./modules";
function main() {
  const yoga = createYoga({
    schema: application.schema,
    context: createContext,
    plugins: [useGraphQLModules(application)],
  });
  const server = createServer(yoga);
  server.listen(4000, () => {
    console.log("Server started on Port no. 4000");
  });
}
main();
We use the @envelop/graphql-modules package and pass in our application, we also pass the application.schmea. 
From your terminal run yarn dev, you will see an error Cannot find module .schema.graphql. Check your build output in the dist directory it has no .graphql files, because esbuild-tsc will only compile TypeScript files to JavaScript. We need to copy our .graphql files into our build folder. To do so first install cpy as a dev dependency -
yarn add -D cpy
Then from the root of the project create etsc.config.js -
module.exports = {
  esbuild: {
    minify: false,
    target: "es2016",
  },
  postbuild: async () => {
    const cpy = (await import("cpy")).default;
    await cpy(
      [
        "src/**/*.graphql", // Copy all .graphql files
        "!src/**/*.{tsx,ts,js,jsx}", // Ignore already built files
      ],
      "dist"
    );
  },
};
Now from the terminal run yarn dev, navigate to localhost:4000/graphql in the browser and play around with the GraphQL API.
Conclusion
We successfully modularized our code base using graphql-modules and created a simple GraphQL API following the schema-first approach. In the next tutorial we will take a look at code-first approach to developing a GraphQL API using graphql-pothos. Until then PEACE.

    
Top comments (0)