Getting Started, GraphQL & Node.js in 2023 - GraphQL Modules
Tags: node, graphql, typescript, prisma
Published: 2023-05-31 10:01:05

### 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](https://github.com/yaldram/todos-graphql-server).

### 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 - 

![folder-structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n2tlr1x0g6u0uf7xiqlk.png)

From your terminal install the following dependencies - 
```bash
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 `.graphql` extension for schema files. To work with `.graphql` files in a Nodejs project we use `graphql-import-node` package.
- Finally, to make our modules work with `graphql-yoga` we use the `@envelop/graphql-modules` package.

### 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` - 
```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 - 
```ts
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` - 
```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 - 
```ts
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 - 
```ts
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 - 
```ts
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` - 
```ts
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 - 
```bash
yarn add -D cpy
``` 

Then from the root of the project create `etsc.config.js` - 
```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.