DocumentationGuidesApollo Server / GraphQL Yoga with Server Preset

Guide: GraphQL Yoga / Apollo Server with Server Preset

GraphQL Code Generator’s server preset, @eddeee888/gcg-typescript-resolver-files, helps GraphQL APIs work at any scale by enforcing best practices such as type-safety and schema module conventions.

Guide

A GraphQL API such as GraphQL Yoga or Apollo Server is the central system where many teams develop their own features without blocking other teams. However, teams may have different standards and practices that can lead to friction. The server preset has features to help solve these issues:

  • Type safety: Resolvers are strictly generated and typed to eliminate the chance of unimplemented resolvers.
  • Schema module conventions: These conventions make ownership clear at domain and code levels to help teams focus.

Setup

1. Create Schema Modules

The server preset works best when the schema is split into smaller modules. This approach keeps each module small and maintainable. So, instead of one schema file, you can split it into smaller schema modules:

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── schema.graphql

Here’s the content of each schema module:

# src/schema/base/schema.graphql
type Query
type Mutation
 
# src/schema/user/schema.graphql
extend type Query {
  user(id: ID!): User
}
type User {
  id: ID!
  fullName: String!
  isAdmin: Boolean!
}
 
# src/schema/book/schema.graphql
extend type Query {
  book(id: ID!): Book
}
extend type Mutation {
  markBookAsRead(id: ID!): Book!
}
type Book {
  id: ID!
  isbn: String!
}

2. Install Server Preset

npm i -D @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files

3. Configure Codegen Config

Create or update your Codegen config as follows:

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files'
 
const config: CodegenConfig = {
  schema: '**/schema.graphql',
  generates: {
    'src/schema': defineConfig()
  }
}
export default config

Remove existing @graphql-codegen/typescript and @graphql-codegen/typescript-resolvers config

The server preset comes with these plugins built-in with stricter defaults. You can override the defaults using the typesPluginsConfig option.

Generate Files

Now, run codegen:

npx graphql-codegen

This results in the following structure:

├── src/
│   ├── schema/
│   │   ├── base/
│   │   │   ├── schema.graphql
│   │   ├── user/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── user.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── User.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── book/
│   │   │   ├── resolvers/
│   │   │   │   ├── Query/
│   │   │   │   │   ├── book.ts            # Generated, changes not overwritten by codegen
│   │   │   │   ├── Mutation/
│   │   │   │   │   ├── markBookAsRead.ts  # Generated, changes not overwritten by codegen
│   │   │   │   ├── Book.ts                # Generated, changes not overwritten by codegen
│   │   │   ├── schema.graphql
│   │   ├── resolvers.generated.ts         # Entirely generated by codegen
│   │   ├── typesDefs.generated.ts         # Entirely generated by codegen
│   │   ├── types.generated.ts             # Entirely generated by codegen

Generated Files Overview

  • types.generated.ts: TypeScript types generated by @graphql-codegen/typescript and @graphql-codegen/typescript-resolvers
  • typeDefs.generated.ts: Static TypeScript Schema AST to be used by the server
  • user/resolvers/Query/user.ts, book/resolvers/Query/book.ts, book/resolvers/Mutation/markBookAsRead.ts: Typed operation resolvers of each module
  • user/resolvers/User.ts, book/resolvers/Book.ts: Typed object type resolvers of each module
  • resolvers.generated.ts: Resolver map that contains all generated operation and object type resolvers

Integration With GraphQL API

We can use generated files in GraphQL API implementation:

src/server.ts
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'http'
import { typeDefs } from './schema/typeDefs.generated'
import { resolvers } from './schema/resolvers.generated'
 
const yoga = createYoga({ schema: createSchema({ typeDefs, resolvers }) })
const server = createServer(yoga)
server.listen(3000)

Implementing Resolvers

Operation resolvers are generated like this example:

src/schema/user/resolvers/Query/user.ts
import type { QueryResolvers } from './../../../types.generated'
export const user: NonNullable<QueryResolvers['user']> = async (_parent, _arg, _ctx) => {
  /* Implement Query.user resolver logic here */
}

Generated operation resolvers always fail TypeScript check without implementation.

This is intentional because it eliminates unimplemented resolvers at runtime.

Object type resolvers are generated like this example:

src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const User: UserResolvers = {
  /* Implement User resolver logic here */
}

All operation and object type resolvers are automatically put into the generated resolver map:

src/schema/resolvers.generated.ts
/* This file was automatically generated. DO NOT UPDATE MANUALLY. */
import type { Resolvers } from './types.generated'
import { book as Query_book } from './book/resolvers/Query/book'
import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'
import { Book } from './book/resolvers/Book'
import { user as Query_user } from './user/resolvers/Query/user'
import { User } from './user/resolvers/User'
export const resolvers: Resolvers = {
  Query: {
    book: Query_book,
    user: Query_user
  },
  Mutation: {
    markBookAsRead: Mutation_markBookAsRead
  },
 
  Book: Book,
  User: User
}

The server preset handles all resolver types and imports. So, you only need to implement your resolver logic.

Conventions to Support Schema Modules

The server preset has module-centric conventions

This means instead of updating the codegen.ts config file, you make changes in each module. This keeps the GraphQL API maintainable at any scale.

Read more about this concept on our blog: Scalable APIs with GraphQL Server Codegen Preset

All conventions are customizable

Check out the documentation for more options.

Adding Custom GraphQL Scalars

GraphQL does not have a lot of scalars by default. Luckily, graphql-scalars has an extensive list of scalars.

The server preset automatically uses scalar implementation from graphql-scalars if it finds a matching name.

First, install graphql-scalars:

npm i graphql-scalars

Then, add a scalar to your schema:

src/schema/base.graphql
type Query
type Mutation
 
# https://github.com/Urigo/graphql-scalars/blob/master/src/scalars/iso-date/DateTime.ts
scalar DateTime

Running codegen automatically imports the scalar implementation into the resolver map:

src/schema/resolvers.generated.ts
import type { Resolvers } from './types.generated'
import { book as Query_book } from './book/resolvers/Query/book'
import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'
import { Book } from './book/resolvers/Book'
import { user as Query_user } from './user/resolvers/Query/user'
import { User } from './user/resolvers/User'
import { DateTimeResolver } from 'graphql-scalars'
export const resolvers: Resolvers = {
  Query: {
    book: Query_book,
    user: Query_user
  },
  Mutation: {
    markBookAsRead: Mutation_markBookAsRead
  },
 
  Book: Book,
  User: User,
  DateTime: DateTimeResolver
}

Furthermore, the type is updated to use the recommended type from graphql-scalars:

src/schema/types.generated.ts
// ... other generated types
 
export type Scalars = {
  ID: { input: string; output: string | number }
  String: { input: string; output: string }
  Boolean: { input: boolean; output: boolean }
  Int: { input: number; output: number }
  Float: { input: number; output: number }
  DateTime: { input: Date | string; output: Date | string } // Type comes from graphql-scalars
}
 
// ... other generated types

The type of any custom scalar is any by default. Without the server preset, you have to configure the DateTime type by manually updating codegen.ts.

Adding Mappers To Chain Resolvers

By default, the generated types make resolvers return objects that match the schema types. However, this means we must handle all field mapping in the root-level resolvers.

This is where we can use mappers to enable resolver chaining. When a mapper is used this way, it can be returned in one resolver, and become the parent argument in the next resolver in the chain.

With the server preset, you can add mappers by exporting interfaces or types with Mapper suffix from *.mappers.ts files in appropriate modules:

src/schema/user/schema.mappers.ts
export interface UserMapper {
  id: string
  firstName: string
  lastName: string
  isAdmin: 'YES' | 'NO'
}

Running codegen again does a few things:

  1. automatically imports and uses this mapper in schema types
src/schema/types.generated.ts
// ... other imports
import { UserMapper } from './user/schema.mappers'
 
export type ResolversTypes = {
  // ... other types
  User: ResolverTypeWrapper<UserMapper>
}
 
export type ResolversParentTypes = {
  // ... other types
  User: UserMapper
}
  1. automatically compares schema type and mapper type to create required field resolvers
src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const User: UserResolvers = {
  fullName: async (_parent, _arg, _ctx) => {
    /* User.fullName resolver is required because User.fullName exists but UserMapper.fullName does not */
  },
 
  isAdmin: ({ isAdmin }, _arg, _ctx) => {
    /* User.isAdmin resolver is required because User.isAdmin and UserMapper.isAdmin are not compatible */
    return isAdmin
  }
}

You can now update your resolvers to use the mapper interface:

// src/schema/user/resolvers/Query/user.ts
import type { QueryResolvers } from './../../../types.generated'
export const user: NonNullable<QueryResolvers['user']> = async (_parent, _arg, _ctx) => {
  return { id: '001', firstName: 'Bart', lastName: 'Simpson', isAdmin: 'YES' }
}
 
// src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const User: UserResolvers = {
  fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`
  isAdmin: ({ isAdmin }) => isAdmin === 'YES'
}