Using GraphQL Yoga with Fastify | Pure DI in TypeScript Node.js

In the tutorial preceding this one, I demonstrated how to create a logging service with metadata. In this article, I will be implementing a new GraphQL endpoint for our application.

I will use the repo as starting point.

Data Store

Before we start implementing the GraphQL server, let's make changes to our business logic layer by adding a new implementation for FindOrderById.

// order-store.ts
interface OrderStore {
  find(id: ID): Promise<Order | undefined>;
}

// find-order-by-id-from-store-provider.ts
export class FindOrderByIdFromStoreProvider {
  constructor(private readonly store: OrderStore) {}

  findOrderById: FindOrderById = (orderId) => {
    return this.store.find(orderId);
  };
}

// registry/find-order-by-id-from-store.ts
export function findOrderById() {
  return ({ orderStore }: { orderStore: OrderStore }) => {
    const { findOrderById } = new FindOrderByIdFromStoreProvider(orderStore);

    return {
      findOrderById,
    };
  };
}

In the code above I also introduced OrderStore interface. We can have multiple implementations for the interface, such as one making requests to SQL DB, or another making HTTP requests to a server.

Essentially, the FindOrderById is our business logic primary port, while OrderStore is a secondary port, in terms of hexagonal architecture.

For now, let's implement the stub adapter for OrderStore:

// stub-order-store.ts
export class StubOrderStore implements OrderStore {
  constructor(private readonly logger: Logger) {}

  find(id: ID): Promise<Order | undefined> {
    this.logger.info(`calling StubOrderStore.find with orderId: ${id}`);

    return Promise.resolve({
      id,
    });
  }
}

// registry/stub-order-store.ts
export function orderStore() {
  return ({ createLogger }: { createLogger: CreateLogger }) => {
    const orderStore = new StubOrderStore(createLogger(StubOrderStore.name));

    return {
      orderStore,
    };
  };
}

// create-app-registry.ts
export function createAppRegistry() {
  return (
    new RegistryComposer()
      // ...
      .add(orderStore())
      .add(findOrderById())
      // ...
      .compose()
  );
}

GraphQL Server

Since I was using fastify I'm going to integrate it with GraphQL Yoga because of simple integration. First of all, we need to install some dependencies:

yarn install graphql graphql-yoga dataloader
yarn install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers ts-node

Now we can add graphqlRoutes FastifyPlugin:

// routes/graphql.ts
type HandleNodeRequest = (
  nodeRequest: NodeRequest
) => Promise<Response> | Response;

export function graphqlRoutes({
  handleGraphQLNodeRequest,
}: {
  handleGraphQLNodeRequest: HandleNodeRequest;
}): FastifyPluginCallback {
  return (fastify, _, next) => {
    fastify.route({
      url: "/graphql",
      method: ["GET", "POST", "OPTIONS"],
      handler: async (req, reply) => {
        const response = await handleGraphQLNodeRequest(req);

        response.headers.forEach((value, key) => {
          reply.header(key, value);
        });

        reply.status(response.status);

        reply.send(response.body);

        return reply;
      },
    });

    next();
  };
}

Here we added a simple route for any HTTP method, that just forwards the request to graphqlYoga.handleNodeRequest.

Let's create the route registrar now:

// registry/graphql-yoga.ts
export function graphqlYoga<
  TServerContext extends Record<string, any> = object,
  TUserContext extends Record<string, any> = object
>(options?: Omit<YogaServerOptions<TServerContext, TUserContext>, "schema">) {
  return ({
    createLogger,
    graphqlSchema,
  }: {
    createLogger: CreateLogger;
    graphqlSchema: GraphQLSchemaWithContext<TUserContext & TServerContext>;
  }) => {
    const graphqlYoga = createYoga<TServerContext, TUserContext>({
      ...options,
      logging: createLogger("GraphQLYoga"),
      schema: graphqlSchema,
    });

    return {
      handleGraphQLNodeRequest: graphqlYoga.handleNodeRequest.bind(graphqlYoga),
    };
  };
}

and add it to our fastifyServer:

// registry/fastify-server.ts
export function fastifyServer() {
  return (
    deps: Parameters<typeof runWithRequestIdPlugin>[0] &
      Parameters<typeof ordersRoutes>[0] &
      Parameters<typeof graphqlRoutes>[0]
  ) => {
    const server = Fastify({});

    server.register(runWithRequestIdPlugin(deps));
    server.register(ordersRoutes(deps));
    server.register(graphqlRoutes(deps));

    return {
      fastifyServer: server,
    };
  };
}

GraphQL Schema

Now we can register our GraphQL schema:

# schema.gql
type Order {
  id: ID!
}

type Query {
  order(id: ID!): Order
}

Using graphql-codegen we can generate the typescript definitions from the schema file, see config here.

// graphql-schema.ts

import { Resolvers } from "./generated/resolvers-types";

export function graphqlSchema() {
  return ({ findOrderById }: { findOrderById: FindOrderById }) => {
    return {
      graphqlSchema: createSchema({
        typeDefs: fs.readFileSync(
          path.join(__dirname, "../schema.gql"),
          "utf8"
        ), // <-- I read schema from file here, but we can also use [esbuild loader](https://github.com/luckycatfactory/esbuild-graphql-loader)
        resolvers: {
          Query: {
            order: (_, args) => findOrderById(args.id),
          },
        } as Resolvers,
      }),
    };
  };
}
// create-app-registry.ts
export function createAppRegistry() {
  return (
    new RegistryComposer()
      // ...
      .add(graphqlSchema())
      .add(graphqlYoga())
      /// ...
      .compose()
  );
}

// index.ts
async function main() {
  const registry = createAppRegistry();

  const server = Fastify({});
  server.register(runWithRequestIdPlugin(registry));
  server.register(ordersRoutes(registry));
  server.register(graphqlRoutes(registry));

  try {
    await server.listen({ port: PORT });

    console.log("listening on port", PORT);
  } catch (err) {
    server.log.error(err);
    process.exit(1);
  }
}

Let's run the app now and try to hit our new /graphql endpoint

POST /graphql HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 80

{"query":"query {\n  order(id: \"order-0\") {\n      id\n  }\n}","variables":{}}

Response:

{
  "data": {
    "order": {
      "id": "order-0"
    }
  }
}

Full example source code can be found here

Conclusion

In this article, we learned how to add the GraphQL Yoga plugin to Fastify using a Pure Dependency Injection registry. In the next article, I'll cover a more interesting topic: how to use DataLoader with GraphQL Yoga and AsyncLocalStorage.