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

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

### Step One: Creating queries using Pothos
Lets install pothos first - 
```bash
yarn add @pothos/core
```
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 - 
```ts
import SchemaBuilder from "@pothos/core";
import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

export const builder = new SchemaBuilder({});

builder.queryType();
builder.mutationType();
```
- 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]!
}
```
Now under the `pothos/todos.ts` file paste - 
```ts
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()
  })
)
```
- 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!
}
```
Under `pothos/todos.ts` paste the following - 
```ts
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,
	 }
      })
  })
)
```
- 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 - 
```ts
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);
      }
    },
  })
);
```
- 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 - 
```ts
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);
      }
    },
  })
);
```
- 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 
```ts
import "./todos";
import "./comments";

import { builder } from "./builder";

// Create and export our graphql schema
export const schema = builder.toSchema();
```
Finally, we will pass this schema `graphql-yoga` under `src/index.ts` - 
```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();
```

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](https://pothos-graphql.dev/docs/plugins) that eases our development, one such plugin is the [prisma plugin](https://pothos-graphql.dev/docs/plugins/prisma) 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 - 
```ts
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"),
  }),
});
```
I would encourage you to check my [repo](https://github.com/yaldram/todos-graphql-server) 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.