nitrogql logonitrogql

Organizing Resolver Definitions

This page describes how to organize your resolver definitions into separate files. This is useful when you have a large schema with many types and fields.

Introduction

In order to write server-side code, you can use nitrogql to generate a Resolvers type that contains all the resolvers for your GraphQL schema. When you schema is very small, you can write all your resolvers in a single file. For example:

import { Resolvers } from "@/app/generated/resolvers";

// Context is an object that is passed to all resolvers.
// It is created per request.
type Context = {};

// define all resolvers.
const resolvers: Resolvers<Context> = {
  Query: {
    me: async () => { /* ... */ },
    todos: async () => { /* ... */ }
  },
  Mutation: {
    toggleTodos: async (_, variables) => { /* ... */ }
  },
  User: {
    // ...
  },
};

As your schema grows, you may want to split your resolvers into separate files. While nitrogql does not provide any special support for organizing resolvers into separate files, you can use your TypeScript wisdom to do this.

Defining resolvers per type

A straightforward way to split your resolver definitions is to define resolvers per type. This can be done with a simple TypeScript syntax:

import { Resolvers } from "@/app/generated/resolvers";

type Context = {};

const queryResolvers: Resolvers<Context>["Query"] = {
  me: async () => { /* ... */ },
  todos: async () => { /* ... */ }
};
const mutationResolvers: Resolvers<Context>["Mutation"] = {
  toggleTodos: async (_, variables) => { /* ... */ }
};
const userResolvers: Resolvers<Context>["User"] = {
  // ...
};

// define all resolvers.
const resolvers: Resolvers<Context> = {
  Query: queryResolvers,
  Mutation: mutationResolvers,
  User: userResolvers,
};

This approach is simple and sometimes sufficient. Splitting resolver definitions into smaller ones should improve developer experience especially when there are type errors.

Splitting Resolvers Into Modules

Often times, you may want to organize server-side implementation into modules, each of which contains a set of related resolvers, not necessarily per type. For example, you may want to have a Todos module which will contain the todos resolver in Query, toggleTodos resolver in Mutation, and also fields in Todo type.

Solution 1: Exporting many resolvers

If you wish to keep it simple, your module can just export each resolver as a function:

import { Resolvers } from "@/app/generated/resolvers";
import { Context } from "@/app/context";

export const todosResolver: Resolvers<Context>["Query"]["todos"] = async () => {
  // ...
};

export const toggleTodosResolver: Resolvers<Context>["Mutation"]["toggleTodos"] = async (_, variables) => {
  // ...
};

export const todoResolvers: Resolvers<Context>["Todo"] = {
  // ...
};

🔦 Note: Above code assumes that you defined a Context type in @/app/context.

Then, you can import and use them in your main resolver definition:

import { Resolvers } from "@/app/generated/resolvers";
import { Context } from "@/app/context";
import { meResolver } from "./session";
import { todosResolver, toggleTodosResolver, todoResolvers } from "./todos";

// define all resolvers.
const resolvers: Resolvers<Context> = {
  Query: {
    me: meResolver,
    todos: todosResolver,
  },
  Mutation: {
    toggleTodos: toggleTodosResolver,
  },
  Todo: todoResolvers,
  // ...
};

Solution 2: Defining resolver definition helpers

If you rather reduce all those type annotations like : Resolvers<Context>["Query"]["todos"], you can define some helper functions to help you define resolvers.

Below code defines a partialResolvers function that accepts a partial resolver definition and returns it as-is.

import { Resolvers } from "@/app/generated/resolvers";
import { Context } from "@/app/context";

function partialResolvers<
  R extends Partial<{
    [K in keyof Resolvers<Context>]: Partial<Resolvers<Context>[K]>;
  }>
>(resolvers: R): R {
  return resolvers;
}

Then, you can use it in your module:


export const todosResolvers = partialResolvers({
  Query: {
    todos: async () => {
      // ...
    }
  },
  Mutation: {
    toggleTodos: async (_, variables) => {
      // ...
    }
  },
  Todo: {
    // ...
  }
});

This way, you still receive the benefit of type checking and contextual typing, while being able to define only resolvers that you want to define in the module. Thanks to the definition of partialResolvers, the resulting type of todosResolvers keeps track of which resolvers are defined.

Then, you need to gather all resolvers from modules and merge them together. To do this in a type-safe way, you can define another helper function:

type UnionToIntersection<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

function mergeResolvers<
  const Rs extends readonly Partial<{
    [K in keyof Resolvers<Context>]: Partial<Resolvers<Context>[K]>;
  }>[]
>(
  resolvers: Rs
): {
  [K in keyof Resolvers<Context>]: UnionToIntersection<
    Rs[number] extends infer U
      ? U extends Record<K, infer V>
        ? V
        : never
      : never
  > & {};
} {
  const result = {} as Record<string, Record<string, unknown>>;
  for (const resolver of resolvers) {
    for (const [key, value] of Object.entries(resolver)) {
      if (result[key] === undefined) {
        result[key] = value;
      } else {
        Object.assign(result[key], value);
      }
    }
  }
  return result as any;
}

Then, you can use it in your main resolver definition:

import { Resolvers } from "@/app/generated/resolvers";
import { Context } from "@/app/context";
import { sessionResolvers } from "./session";
import { todosResolvers } from "./todos";

// Note: `: Resolvers<Context>` is important to guarantee that you
// defined all required resolvers.
const resolvers: Resolvers<Context> = mergeResolvers([
  sessionResolvers,
  todosResolvers,
]);

This way, you can split resolver definitions as you like, while maintaining type safety. Only caveat is that these helper functions look too magical. 😅

👻 Note: above helpers are not thoroughly tested. You can use them in your project, but please be aware that they might have some bugs.

⚖️ Note: Code in this page is licensed under the MIT license.