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
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();
- We first created our 
PrismaClient, that we will use for querying our database. - We then created what is known as a 
builderusing theSchemaBuilderclass from pothos. - Finally, we create the base query & mutation which is equivalent to 
type Query & type Mutationin theschema firstworld. - 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 -
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 
objectReffunction to declare ourtype Todo.objectRefallows us to build modular code. - Next we added our 
enum TaskStatushereprismatypes come in handy, we passed theTaskStatusfromprisma. - Then we implement our 
Todotype, we are resolving all the fields, notice we usedparent.statusto resolve theenumTypefield, well whats on the parent ? - The answer lies on the next lines, we implement our Todo Query 
type Query { todos: [Todo]! }, we use thequeryFieldfunction and pass the type asTodosObjectand then resolve the query. - So, on the 
parent.statuswe get the above query result, and then we are resolving the status field. - Our 
type Todoalso has acommentsfield, we will resolve it later as we have not yet createdtype 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 -
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 CreateTodoInputtype. - 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);
      }
    },
  })
);
- We created a 
Commenttype we add a querycommentand resolved it. - Similarly, we create a 
inputand 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);
      }
    },
  })
);
- We added 2 new mutations for edit & delete with their respective input types.
 - Also, note under the 
TodosObjectimplementation we resolved thecommentsfield, we passed in thetypeand 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();
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();
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"),
  }),
});
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)