DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

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

Introduction

This is part three of our tutorial series. In the previous tutorial we created a Todos GraphQL server, with modular architecture following schema first approach. In this tutorial we will implement the same Todos GraphQL server this time with code first approach using graphql-pothos.

Overview

Pothos is a library that helps us in building GraphQL servers using code first approach. In the schema first approach we create a schema, sometimes in a separate .graphql file and then the respective resolvers in a resolver.ts file. Then there is another issue, types, how to type our resolvers, though we have used types from prisma/client in our tutorials but when it comes to the inputs for mutations these clearly fall short. There are codegen tools too, but these are not ideal sometimes. With code first approach we create our schema using TypeScript and resolve it. I recommend you go through the Pothos docs & examples once.

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

  • Creating queries using Pothos.
  • Creating mutations using Pothos.
  • Setup Comment schema & resolvers.
  • Setup Todo schema & resolvers.
  • Stitch pothos schema to graphql-yoga
  • Introducing Pothos plugins.

All the code for this tutorial is available under the graphql-pothos branch, check the repo.

Step One: Creating queries using Pothos

Lets install pothos first -

yarn add @pothos/core
Enter fullscreen mode Exit fullscreen mode

Now under src create a new folder pothos. Under pothos create 4 new files namely index.ts, builder.ts, todos.ts, comments.ts. We are modularizing our codebase, all the Todos & Comments related code will be in the respective files. Under the builder.ts file paste -

import SchemaBuilder from "@pothos/core";
import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

export const builder = new SchemaBuilder({});

builder.queryType();
builder.mutationType();
Enter fullscreen mode Exit fullscreen mode
  • We first created our PrismaClient, that we will use for querying our database.
  • We then created what is known as a builder using the SchemaBuilder class from pothos.
  • Finally, we create the base query & mutation which is equivalent to type Query & type Mutation in the schema first world.
  • We will be using this builder object to create rest of the types like Todos, Comments, TodosInput, etc.

Now we will create and resolve our type Todo with following the code first approach this is our 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]!
}
Enter fullscreen mode Exit fullscreen mode

Now under the pothos/todos.ts file paste -

import { TaskStatus, Todos } from "@prisma/client"
import { builder, prisma } from "./builder"

const TodosObject = builder.objectRef<Todos>("Todo");

builder.enumType(TaskStatus, {
   name: "TaskStatus"
})

TodosObject.implement({
  fields: (t) => ({
     id: t.exposeID("id"),
     task: t.exposeString("task"),
     description: t.exposeString("description"),
     status: t.field({
        type: TaskStatus,
        resolve: (parent) => parent.status
      }),
      tags: t.exposeStringList("tags"),
   })
})

builder.queryField("todos", (t) => 
  t.field({
     type: [TodosObject],
     resolve: () => prisma.todos.findMany()
  })
)
Enter fullscreen mode Exit fullscreen mode
  • We first use the objectRef function to declare our type Todo. objectRef allows us to build modular code.
  • Next we added our enum TaskStatus here prisma types come in handy, we passed the TaskStatus from prisma.
  • Then we implement our Todo type, we are resolving all the fields, notice we used parent.status to resolve the enumType field, well whats on the parent ?
  • The answer lies on the next lines, we implement our Todo Query type Query { todos: [Todo]! }, we use the queryField function and pass the type as TodosObject and then resolve the query.
  • So, on the parent.status we get the above query result, and then we are resolving the status field.
  • Our type Todo also has a comments field, we will resolve it later as we have not yet created type Comment.

Step Two: Creating mutations using Pothos

Let us now create our first mutation, for that we need to first create the Input type and create a new mutation addTodo on the main type Mutation resolve it -

input TodoInput {
  task: String!
  description: String!
  status: TaskStatus!
  tags: [String]
}

type Mutation {
  addTodo(input: TodoInput): Todo!
}
Enter fullscreen mode Exit fullscreen mode

Under pothos/todos.ts paste the following -

const CreateTodoInput = builder.inputType("CreateTodoInput", {
  fields: (t) => ({
    task: t.string({ required: true }),
    description: t.string({ required: true }),
    status: t.field({
      type: TaskStatus,
      required: true,
    }),
    tags: t.stringList({ required: true }),
  })
})

builder.mutationField("addTodo", (t) =>
  t.field({
    type: TodosObject,
    args: {
     input: t.arg({ type: CreateTodoInput, required: true })
    },
    resolve: (parent, args) =>
      prisma.todos.create({
     data: {
       task: args.input.task,
       description: args.input.description,
           status: args.input.status,
       tags: args.input.tags,
     }
      })
  })
)
Enter fullscreen mode Exit fullscreen mode
  • We first created our input CreateTodoInput type.
  • Then we created our mutation type Mutation { addTodo...} and resolved it, with full type safety we can safely create our schema and resolve it.

Step Three: Setup Comments schema & resolvers

Let us know implement the Comment type first under pothos/comments.ts paste -

import { Comments } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";

import { builder, prisma } from "./builder";

export const CommentsObject = builder.objectRef<Comments>("Comment");

CommentsObject.implement({
  fields: (t) => ({
    id: t.exposeID("id"),
    body: t.exposeString("body"),
  }),
});

builder.queryField("comment", (t) =>
  t.field({
    type: CommentsObject,
    args: {
      id: t.arg.string({ required: true }),
    },
    resolve: async (parent, args, context) => {
      try {
        const comment = await prisma.comments.findUniqueOrThrow({
          where: {
            id: args.id,
          },
        });

        return comment;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code === "P2025"
        ) {
          return Promise.reject(
            new GraphQLError(`Cannot find comment with id ${args.id}`)
          );
        }

        return Promise.reject(error);
      }
    },
  })
);

const CreateCommentInput = builder.inputType("CreateCommentInput", {
  fields: (t) => ({
    todoId: t.string({ required: true }),
    body: t.string({ required: true }),
  }),
});

builder.mutationField("postCommentOnTodo", (t) =>
  t.field({
    type: CommentsObject,
    args: {
      input: t.arg({ type: CreateCommentInput, required: true }),
    },
    resolve: async (parent, args) => {
      try {
        const comment = await prisma.comments.create({
          data: {
            body: args.input.body,
            todoId: args.input.todoId,
          },
        });
        return comment;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code == "P2003"
        ) {
          return Promise.reject(
            new GraphQLError(
              `Cannot add comment on a non-exiting todo with id ${args.input.todoId}`
            )
          );
        }
        return Promise.reject(error);
      }
    },
  })
);
Enter fullscreen mode Exit fullscreen mode
  • We created a Comment type we add a query comment and resolved it.
  • Similarly, we create a input and added a mutation to add comments pretty straightforward.

Step Four: Setup Todo schema & resolvers

Now under pothos/todos.ts paste the final code -

import { TaskStatus, Todos } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { GraphQLError } from "graphql";

import { builder, prisma } from "./builder";
import { CommentsObject } from "./comments";

export const TodosObject = builder.objectRef<Todos>("Todo");

builder.enumType(TaskStatus, {
  name: "TaskStatus",
});

TodosObject.implement({
  fields: (t) => ({
    id: t.exposeID("id"),
    task: t.exposeString("task"),
    description: t.exposeString("description"),
    status: t.field({
      type: TaskStatus,
      resolve: (parent) => parent.status,
    }),

    tags: t.exposeStringList("tags"),
    comments: t.field({
      type: [CommentsObject],
      resolve: (parent) =>
        prisma.comments.findMany({
          where: {
            todoId: parent.id,
          },
        }),
    }),
  }),
});

builder.queryField("todos", (t) =>
  t.field({
    type: [TodosObject],
    resolve: () => prisma.todos.findMany(),
  })
);

const CreateTodoInput = builder.inputType("CreateTodoInput", {
  fields: (t) => ({
    task: t.string({ required: true }),
    description: t.string({ required: true }),
    status: t.field({
      type: TaskStatus,
      required: true,
    }),
    tags: t.stringList({ required: true }),
  }),
});

builder.mutationField("addTodo", (t) =>
  t.field({
    type: TodosObject,
    args: {
      input: t.arg({ type: CreateTodoInput, required: true }),
    },
    resolve: (parent, args) =>
      prisma.todos.create({
        data: {
          task: args.input.task,
          description: args.input.description,
          status: args.input.status,
          tags: args.input.tags,
        },
      }),
  })
);

const EditTodoInput = builder.inputType("EditTodoInput", {
  fields: (t) => ({
    id: t.string({ required: true }),
    task: t.string({ required: true }),
    description: t.string({ required: true }),
    status: t.field({
      type: TaskStatus,
      required: true,
    }),
    tags: t.stringList({ required: true }),
  }),
});

builder.mutationField("editTodo", (t) =>
  t.field({
    type: TodosObject,
    args: {
      input: t.arg({ type: EditTodoInput, required: true }),
    },
    resolve: async (parent, args) => {
      const todoId = args.input.id;

      try {
        const todo = await 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 == "P2025"
        ) {
          return Promise.reject(
            new GraphQLError(`Cannot edit a non-exiting todo with id ${todoId}`)
          );
        }
        return Promise.reject(error);
      }
    },
  })
);

const DeleteTodoInput = builder.inputType("DeletesTodoInput", {
  fields: (t) => ({
    id: t.string({ required: true }),
  }),
});

builder.mutationField("deleteTodo", (t) =>
  t.field({
    type: TodosObject,
    args: {
      input: t.arg({ type: DeleteTodoInput, required: true }),
    },
    resolve: async (parent, args) => {
      const todoId = args.input.id;

      try {
        const todo = await prisma.todos.delete({
          where: {
            id: todoId,
          },
        });

        return todo;
      } catch (error) {
        if (
          error instanceof PrismaClientKnownRequestError &&
          error.code == "P2025"
        ) {
          return Promise.reject(
            new GraphQLError(
              `Cannot delete a non-exiting todo with id ${todoId}`
            )
          );
        }
        return Promise.reject(error);
      }
    },
  })
);
Enter fullscreen mode Exit fullscreen mode
  • We added 2 new mutations for edit & delete with their respective input types.
  • Also, note under the TodosObject implementation we resolved the comments field, we passed in the type and ran the database query.

Step Five: Stitch pothos schema to graphql-yoga

Under pothos/index.ts lets create a schema from our builder

import "./todos";
import "./comments";

import { builder } from "./builder";

// Create and export our graphql schema
export const schema = builder.toSchema();
Enter fullscreen mode Exit fullscreen mode

Finally, we will pass this schema graphql-yoga under src/index.ts -

import "graphql-import-node";

import { createServer } from "http";
import { createYoga } from "graphql-yoga";

import { schema } from "./pothos";

function main() {
  const yoga = createYoga({
    schema: schema,
  });

  const server = createServer(yoga);

  server.listen(4000, () => {
    console.log("Server started on Port no. 4000");
  });
}

main();
Enter fullscreen mode Exit fullscreen mode

From the terminal run yarn dev and play around with our GraphQL endpoint, test all the queries & mutations.

Step Six: Pothos plugins

Pothos also has a set of plugins that eases our development, one such plugin is the prisma plugin it has a lot of features like relations, sorting, etc. For example, in the pothos/todos.ts file for creating the Todo type instead of using objectRef we can use the prismaObject on the builder and resolve relations easily like so -

builder.prismaObject("Todos", {
  fields: (t) => ({
    id: t.exposeID("id"),
    task: t.exposeString("task"),
    description: t.exposeString("description"),
    status: t.field({
      type: TaskStatus,
      resolve: (parent) => parent.status,
    }),
    tags: t.exposeStringList("tags"),
    comments: t.relation("Comments"),
  }),
});
Enter fullscreen mode Exit fullscreen mode

I would encourage you to check my repo for this tutorial series, you can find the pothos-prisma folder on the master branch.

Conclusion

There you go, we finally implemented our GraphQL server using the code first approach, I personally would use this approach for my GraphQL servers. I hope this tutorial series gave a good introduction to getting started with GraphQL in 2023. In the next tutorial we will create a simple React Frontend using urql. Until next time PEACE.

Top comments (0)