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
builder
using theSchemaBuilder
class from pothos. - Finally, we create the base query & mutation which is equivalent to
type Query & type Mutation
in theschema 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 -
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 ourtype Todo
.objectRef
allows us to build modular code. - Next we added our
enum TaskStatus
hereprisma
types come in handy, we passed theTaskStatus
fromprisma
. - Then we implement our
Todo
type, we are resolving all the fields, notice we usedparent.status
to resolve theenumType
field, well whats on the parent ? - The answer lies on the next lines, we implement our Todo Query
type Query { todos: [Todo]! }
, we use thequeryField
function and pass the type asTodosObject
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 acomments
field, 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 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);
}
},
})
);
- We created a
Comment
type we add a querycomment
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);
}
},
})
);
- We added 2 new mutations for edit & delete with their respective input types.
- Also, note under the
TodosObject
implementation we resolved thecomments
field, we passed in thetype
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();
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)