19 Sept 2022
Pothos is a plugin based GraphQL schema builder for TypeScript.
Pothos comes with a huge collection of plugins, but there's nothing needed to get started to benefit from type-safety than Pothos itself.
We'll be using GraphQL Yoga as our server. We'll pass the schema Pothos generates to Yoga, and serve that!
Let's first install Yoga and instantiate a default server.
npm install -E @graphql-yoga/node graphql
Now inside server.ts
we'll import and invoke createServer
, as well as calling .start()
:
import { createServer } from "@graphql-yoga/node";
const server = createServer({});
server.start();
Now let's install Pothos:
npm install @pothos/core
Then inside of server.ts
we'll import and instantiate SchemaBuilder
:
import SchemaBuilder from "@pothos/core";
const builder = new SchemaBuilder();
Then where we call createServer
we'll pass important schema
value. For this we can use .toSchema()
function provided by Pothos schema builder:
const server = createServer({
schema: builder.toSchema(),
});
Before we begin implementing any types, or "resolvers" for our GraphQL schema, we'll create some TypeScript types!
We'll be building a simple shopping cart API that responds to fetching a cart by ID, and adding items to the cart!
type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type Cart = {
id: string;
items?: CartItem[];
};
With these types we can tell Pothos about the different "objects", and "scalars" to expect from our schema. Here we assign the types for Cart
and CartItem
to the TypeScript types, and configure the scalar ID
to use string
type for its accessors:
const builder = new SchemaBuilder<{
Objects: {
Cart: Cart;
CartItem: CartItem;
};
Scalars: {
ID: { Input: string; Output: string };
};
}>();
Now we'll add some dummy data for our API in the shape of carts above:
const CARTS = [
{ id: "1", items: [{ id: "1", name: "My item", price: 1000, quantity: 1 }] },
];
Now we'll define the objectType
for our Cart
type. We want to "expose" the ID of items as well as the items in the cart.
builder.objectType("Cart", {
fields: (t) => ({
id: t.exposeString("id"),
items: t.field({
type: ["CartItem"],
resolve: (cart) => cart.items ?? [],
}),
}),
});
For items
above we tell Pothos that this will be a GraphQL list by using the ["CartItem"]
value.
We'll next create the objectType
for the CartItem
object. Similar to above we'll expose the actual values from the CARTS[x].items
array above:
builder.objectType("CartItem", {
fields: (t) => ({
id: t.exposeString("id"),
name: t.exposeString("name"),
quantity: t.exposeInt("quantity"),
}),
});
You'll notice we used exposeInt
for the quantity
field which tells Pothos this field is of the GraphQL scalar Int
.
Finally we'll create the queryType
for fetching a cart by id
. We'll specify the field cart
on the queryType
, set it as nullable, set a required argument, and resolve data from the CARTS
array where the id
passed to the arguments match:
builder.queryType({
fields: (t) => ({
cart: t.field({
nullable: true,
args: {
id: t.arg.id({ required: true }),
},
type: "Cart",
resolve: (_, { id }) => {
const cart = CARTS.find((c) => c.id === id);
if (!cart) {
throw new Error(`No cart with ID: ${id}`);
}
return cart;
},
}),
}),
});
That's it! That's all we need to now run our GraphQL Yoga server:
npm run dev # ts-node-dev server.ts
Now we can head to http://localhost:4000/graphql
and execute the following query to return our cart by ID:
{
cart(id: "1") {
id
items {
id
name
quantity
}
}
}
We can also explore the Documentation Explorer to see our query, and types. You'll notice there's no descriptions (because we haven't added any), but we can see which fields are nullable and non-nullable.
It would be useful for our shopping cart API to have a subTotal
for the cart that returns the amount
and formatted
amount for all items in the cart, as well as the same values for cart items (for both the individual item, and line total — quantity x price
).
We'll use the special objectRef
method from Pothos to do this:
const Money = builder.objectRef<number>("Money").implement({
fields: (t) => ({
amount: t.int({ resolve: (amount) => amount }),
formatted: t.string({
resolve: (amount) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount / 100),
}),
}),
});
Now we can use this inside of the types we previously created for Cart
, and CartItem
:
builder.objectType("Cart", {
fields: (t) => ({
id: t.exposeString("id"),
items: t.field({
type: ["CartItem"],
resolve: (cart) => cart?.items ?? [],
}),
subTotal: t.field({
type: Money,
resolve: (cart) =>
cart.items?.reduce(
(acc, item) => acc + item.price * item.quantity,
0
) ?? 0,
}),
}),
});
builder.objectType("CartItem", {
fields: (t) => ({
id: t.exposeString("id"),
name: t.exposeString("name"),
quantity: t.exposeInt("quantity"),
lineTotal: t.field({
type: Money,
resolve: (item) => item.price * item.quantity,
}),
unitTotal: t.field({
type: Money,
resolve: (item) => item.price,
}),
}),
});
Finally to finish our shopping cart API we'll add a mutation using only code to add items to our CARTS
array, and return the new items, quantities, and sub totals.
There's a few things we'll need to do there:
addItem
Cart
items
Before we begin we'll install a plugin by Pothos that lets us wrap our arguments with input
:
npm install -E @pothos/plugin-with-input
Then let's update our instance of SchemaBuilder
with our newly installed plugin:
const builder = new SchemaBuilder<{
Objects: {
Cart: Cart;
CartItem: CartItem;
};
Scalars: {
ID: { Input: string; Output: string };
};
}>({
plugins: [WithInputPlugin],
});
Now we can continue to create our mutationType
:
builder.mutationType({
fields: (t) => ({
addItem: t.fieldWithInput({
input: {
cartId: t.input.id({ required: true }),
id: t.input.id({ required: true }),
name: t.input.string({ required: true }),
price: t.input.int({ required: true }),
quantity: t.input.int({ defaultValue: 1, required: true }),
},
type: "Cart",
resolve: (_, { input: { cartId, ...input } }) => {
const cart = CARTS.find((c) => c.id === cartId);
if (!cart) {
throw new Error(`No cart with ID: ${cartId}`);
}
return {
id: cartId,
items: [...cart?.items, input],
};
},
}),
}),
});
That's it!