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.
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:
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 { createServer } from 'http'import { createSchema, createYoga } from 'graphql-yoga'import { resolvers } from './schema/resolvers.generated'import { typeDefs } from './schema/typeDefs.generated'const yoga = createYoga({ schema: createSchema({ typeDefs, resolvers }) })const server = createServer(yoga)server.listen(3000)
src/server.ts
import { ApolloServer } from 'apollo-server'import { resolvers } from './schema/resolvers.generated'import { typeDefs } from './schema/typeDefs.generated'const server = new ApolloServer({ typeDefs, resolvers })// The `listen` method launches a web serverserver.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`)})
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 */}
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 { Book } from './book/resolvers/Book'import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'import { book as Query_book } from './book/resolvers/Query/book'import type { Resolvers } from './types.generated'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
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
yarn add graphql-scalars
pnpm add graphql-scalars
bun add graphql-scalars
Then, add a scalar to your schema:
src/schema/base.graphql
type Querytype Mutation# https://github.com/Urigo/graphql-scalars/blob/master/src/scalars/iso-date/DateTime.tsscalar DateTime
Running codegen automatically imports the scalar implementation into the resolver map:
src/schema/resolvers.generated.ts
import { DateTimeResolver } from 'graphql-scalars'import { Book } from './book/resolvers/Book'import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'import { book as Query_book } from './book/resolvers/Query/book'import type { Resolvers } from './types.generated'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, DateTime: DateTimeResolver}
Furthermore, the type is updated to use the recommended type from graphql-scalars:
src/schema/types.generated.ts
// ... other generated typesexport 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:
automatically imports and uses this mapper in schema types
src/schema/types.generated.ts
// ... other importsimport { UserMapper } from './user/schema.mappers'export type ResolversTypes = { // ... other types User: ResolverTypeWrapper<UserMapper>}export type ResolversParentTypes = { // ... other types User: UserMapper}
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: