DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Getting Started, GraphQL & Node.js in 2023 - GraphQL Modules

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 -

folder-structure

From your terminal install the following dependencies -

yarn add graphql-modules graphql-import-node @envelop/graphql-modules 
Enter fullscreen mode Exit fullscreen mode
  • 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!
}
Enter fullscreen mode Exit fullscreen mode

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);
      }
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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);
      }
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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],
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

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)