In the Market for a GraphQL server? Try GraphQL Yoga

My team recently stood up a GraphQL API endpoint with a server called Yoga, and I was pleased with how easy it was to get going. Here’s an overview of what we did and why I think Yoga is worth a look.

Yoga?

Yoga is a GraphQL Server, like Apollo. You wire it into an endpoint in your backend. There, it receives GraphQL queries, does a bunch of processing with your specification and logic, and returns results. It’s maintained by The Guild. That’s the organization behind the excellent graphql-code-generator.

The name “Yoga” is unfortunately not very Google-able, but it’s not the worst offender in the ecosystem (I’m looking at you, Golang.). It helps to use “Yoga server” or “Yoga GraphQL” in your searches.

Getting Started

We’ll start by declaring a new API endpoint. On my Next.js project, this is an API route, which is expected to contain a default export of an Express-like HTTP handler. Yoga’s createYoga helper produces a compatible handler:


// pages/api/graphql.ts
import { createSchema, createYoga } from "graphql-yoga";

export const yogaServer = createYoga({
  graphqlEndpoint: "/api/graphql",
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          "This is the `greetings` field of the root `Query` type",
      },
    },
  }),
});

export default yogaServer;

..that’s it! Next, we’ll test it — but wait! Don’t fire up your GraphQL client yet. First, visit the new endpoint in a browser, and there’s an interactive GraphiQL instance waiting for you:

Yoga GraphQL

I’d also like to point out that you can customize GraphiQL’s initial text, which is a great place to paste a few of your app’s common queries.

Next, we’ll get the schema and resolvers out of this file.

Wrangling the Schema

There are a bunch of ways to define your GraphQL Schema, but we’re using a simple .graphql file:


import typeDefs from "../../graphql/schema.graphql";

export const yogaServer = createYoga({
  /* ... */
  schema: createSchema({ typeDefs, resolvers: {/* ... */} }),
});

To make this work, we added a loader to WebPack:


const nextConfig = {
  /* ... */
  webpack: (config) => {
    config.module.rules.push({
      test: /.(graphql|gql)$/,
      exclude: /node_modules/,
      loader: "graphql-tag/loader",
    });
    return config;
  },
};

Wrangling the Resolvers

The resolvers are a little more complicated. We keep them in separate TypeScript files, using types generated by the aforementioned graphql-code-generator’s typescript-resolvers plugin. It winds up looking like this:


import { Resolvers } from "./resolver-types.gen";

export const resolvers: Resolvers = {
  Query: {
    greetings: (parent, args, ctx, info) => "This is the `greetings` field of the root `Query` type",
  },
  Mutation: {/* .. */}
}

Custom Context Type

We’ll want a custom context to hold things like the currently logged-in user, database connection, etc.

First, we add it to the Yoga server setup like this:


/** Next.js API context */
export type ServerContext = {
  req: NextApiRequest;
  res: NextApiResponse;
};

/** Our custom context */
export type AppContext = {
  requestedUrl: string;
  prisma: PrismaClient;
};

export type ResolverContext = AppContext & ServerContext;

export const yogaServer = createYoga<ServerContext, AppContext>({
  /* ... */
  context: async ({ req, res, request }) => {
    console.log("you can see request info:", req.headers["content-length"]);
    const appContext: AppContext = {
      prisma: getPrismaSingleton(),
    };
    return appContext;
  },
});

Yoga will pass this context to our resolvers. To make sure we reflected this in our resolvers’ parameter types, we tell graphql-code-generator about it with the contexttype option. For reference, here’s our current codegen config:


import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "./src/graphql/schema.graphql",
  documents: ["src/graphql/queries/*.ts"],
  generates: {
    "./src/graphql/resolvers/resolver-types.gen.ts": {
      plugins: [
        "typescript",
        "typescript-resolvers",
        { add: { content: "import { DeepPartial } from 'utility-types';" } },
      ],
      config: {
        contextType: "../../pages/api/graphql#ResolverContext",
        defaultMapper: "DeepPartial<{T}>",
      },
    },
    "./src/graphql/client.gen/": {
      preset: "client",
      plugins: [],
    },
  },
  hooks: { afterAllFileWrite: ["../node_modules/.bin/prettier"] },
};
export default config;

Authentication with Auth0

We’re using Auth0 as an identity provider, which offers some nice Next.js integration. The API endpoint can be made to require login by wrapping the handler (withApiAuthRequired()), and the session is available behind getSession():


import { getSession, Session, withApiAuthRequired } from "@auth0/nextjs-auth0";

export const yogaServer = createYoga<ServerContext, AppContext>({
  /* ... */
  context: async ({ req, res, request }) => {
    const session = await getSession(req, res);
    if (!session) {
      throw new Error("Unable to retrieve session.");
    }

    const appContext: AppContext = {
      prisma: getPrismaSingleton(),
      session, // <---
    };
    return appContext;
  },
});

export default withApiAuthRequired(yogaServer);

Now the user info is available in resolver implementations:


export const resolvers: Resolvers = {
  Query: {
    greetings: (parent, args, ctx, info) =>
      `Greetings, ${ctx.session.user.given_name}!`,
  }
}

Odds and Ends

This is getting long, but I'd like to point out two last features:

First, Yoga is extensible with a plugin ecosystem. To try it out, I threw together a middleware that logs each Query:


import { Plugin } from "@envelop/core";

const beforeAfterGqlExecute: Plugin<ServerContext, AppContext> = {
  onExecute: async ({ args }) => {
    console.log(`GQL  --> ${summarizeQuery(args)}`);
    return {
      async onExecuteDone(payload) {
        console.log(`GQL  <-- ${JSON.stringify(payload.result)}`);
        return;
      },
    };
  },
};

export const yogaServer = createYoga({
  /* ... */
  plugins: [beforeAfterGqlExecute],
});

Lastly, Yoga offers an in-memory fetch implementation, which is great for testing. GraphQL clients like URQL allow you to pass in your own fetch. We stitched together our App's GraphQL client and Yoga's in-memory fetch. That means we're able to invoke queries from unit tests, with tooling and client logic similar to our front end.

Wrapping Up

We're just getting started, but, so far, I'm impressed with Yoga. While I'm using Yoga with Next.js, it's standards-based and portable, compatible with various API frameworks and even non-Node execution environments like Deno and CloudFlare workers.

Our project came together with less effort than some other stacks I've worked in. Next time you're in the market for a GraphQL server, I recommend you give Yoga a look!

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *