Type-safe GraphQL resolvers with garph

17 Apr 2023

Let's begin with a basic GraphQL Yoga setup. I'll be using Cloudflare Workers but you can serve GraphQL however you want.

npx wrangler init hello-garph

Next we'll install GraphQL and GraphQL Yoga:

npm i graphql graphql-yoga

If we now begin to construct the GraphQL Yoga server using createYoga and createSchema, we'll notice that when we define the resolvers we get absolutely no type safety.

import { createYoga, createSchema } from "graphql-yoga";

const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      cart(id: ID!): Cart!
    }

    type Cart {
      id: ID!
      totalItems: Int!
      items: [CartItem]
      subTotal: Money!
    }

    type CartItem {
      id: ID!
      name: String!
    }

    type Money {
      amount: Int!
      formatted: String!
    }
  `,
  resolvers: {
    Query: {
      cart: (_, { id }) => ({
        id,
        totalItems: 1,
        items: [{ id: "item-1", name: "Stickers" }],
        subTotal: {
          amount: 1000,
          formatted: "£10",
        },
      }),
    },
  },
});

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const yoga = createYoga({
      graphqlEndpoint: "/",
      landingPage: false,
      schema,
    });

    return yoga.fetch(request, env, ctx);
  },
};

We explored in episode 26 how we can use the GraphQL Code Generator to automatically create types for resolvers.

But what if I told you there was another way...

Introducing garph

garph is a great tool that can be used to build the schema, and resolver types by using just code. If you've seen Pothos or Nexus, this will be familiar.

Let's first install garph using NPM:

npm install garph

Now let's update the src/index.ts and remove the schema const and createSchem import. We'll use garph instead.

import { createYoga } from "graphql-yoga";

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const yoga = createYoga({
      graphqlEndpoint: "/",
      landingPage: false,
      schema,
    });

    return yoga.fetch(request, env, ctx);
  },
};

Now add the garph imports to our src/index.ts:

import { g, InferResolvers, buildSchema } from "garph";

We had earlier the SDL for our Query type:

type Query {
  cart(id: ID!): Cart!
}

We can use the import g from garph to reference cart, pass some args and a description:

import { g, InferResolvers, buildSchema } from "garph";

const query = g.type("Query", {
  cart: g
    .ref(cart)
    .args({ id: g.id() })
    .description("The ID of the Cart you want to retrieve".),
});

It's that easy! This will generate the same GraphQL schema we wrote earlier:

type Query {
  cart(id: ID!): Cart!
}

You'll notice we're referencing cart in the query type but we haven't yet defined this. Let's do that now:

const cart = g.type("Cart", {
  id: g.id(),
  totalItems: g.int(),
  items: g.ref(cartItem).list(),
  subTotal: g.ref(money),
});

Now let's finish by creating the cartItem and money types:

const cartItem = g.type("CartItem", {
  id: g.id(),
  name: g.string(),
  description: g.string().optional(),
});

const money = g.type("Money", {
  amount: g.int(),
  formatted: g.string(),
});

If we wanted to make CartItem a union type, we can do that with garph too:

const promotionItem = g.type("PromotionCartItem", {
  id: g.id(),
  name: g.string(),
  description: g.string().optional(),
});

const skuItem = g.type("SkuCartItem", {
  id: g.id(),
  name: g.string(),
  description: g.string().optional(),
});

const cartItem = g.unionType("CartItem", {
  skuItem,
  promotionItem,
});

Now all that's left to do is create a new resolvers map and use the special import InferResolvers, passing in a generic for the Query type:

const resolvers: InferResolvers<
  { Query: typeof queryType },
  { context: YogaInitialContext & { env: Env } }
> = {
  Query: {
    // ...
  },
};

Now when we begin to type inside the resolver map for Query we have full type safety!

const resolvers: InferResolvers<
  { Query: typeof queryType },
  { context: YogaInitialContext & { env: Env } }
> = {
  Query: {
    cart: (_, { id }) => ({
      id,
      totalItems: 1,
      items: [{ id: "item-1", name: "Stickers" }],
      subTotal: {
        amount: 1000,
        formatted: "£10",
      },
    }),
  },
};

You'll notice we haven't had to configure any tools to generate code but simply import the utilites from garph to generate our type safe schema!

To finish and connect this to GraphQL Yoga all you need to do is invoke buildSchema:

const schema = buildSchema({ g, resolvers });

interface Env {}

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const yoga = createYoga({
      graphqlEndpoint: "/",
      landingPage: false,
      schema,
    });

    return yoga.fetch(request, env, ctx);
  },
};