How to Build Schema in GraphQL

A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend

E.Y.
5 min readApr 30, 2021
Photo by Ratapan Anantawat on Unsplash

This is a 6th of the series blogs on deep dive into Apollo GraphQL from backend to frontend. A lot of information is Apollo GraphQL Doc or GraphQL Doc as well as their source code on Github — all tributes go to them. For my part, I would like to give you my “destructuring” of the original knowledge and my reflection on it, analysis on the code examples/source code as well as some extra examples.

Sometimes, probably in older projects, you may see some other ways of building schemas using buildSchema , or makeExecutableSchema in a lot of newer projects. And sometimes, you can see a schema is built using GraphQLObjectType . So what are their relations and differences? And how are they link with apollo-server when we initiate a server instance?

First, let’s look at BuildSchema vs GraphQLSchema.

BuildSchema & GraphQLSchema — the resolver problem

Let’s see an example:

=======BuildSchemaconst { graphql, buildSchema } = require('graphql');const schema = buildSchema(`
type Query {
hello: String
}
`);
const root = { hello: () => 'Hello world!' };graphql(schema, '{ hello }', root).then((response) => {
console.log(response);
});
==========GraphQLSchemaconst { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql');const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: () => ({
hello: {
type: GraphQLString,
resolve: () => 'Hello world!'
}
})

})
});
graphql(schema, '{ hello }').then((response) => {
console.log(response);
});

So you can see the main difference is that

  • Using buildSchema uses SDL to define the schema, while
  • Using GraphQLSchema leverages `GraphQLSchema class and uses GraphQLObjectType inside to build individual types.

Both methods will return a GraphQLSchema object.

But have you noticed what is missing or unusual from the example above? Yes there is no resolvermap explicitly defined.

Well, this not accurate. If you were to use the GraphQLObjectType to construct the schema like in the example above, there is an optional resolve function. But this is the only implementation of a field resolver.

For buildSchema , this is true. In the example above, we are executing the query directly using the graphql function. HowbuildSchema gets around this is by relying on GraphQL's default resolver behaviour and passing in a root value as shown in the example above.

In GraphQL, if a field does not have a resolve function, GraphQL will examine the parent value returned by the parent field's resolver and, assuming it is an Object, will try to find a property on that parent value that matches the field name. If it finds a match, it resolves the field to that value or call the function corresponding to the property and then resolves to the value returned by the function,

In the example above, the hello field has no resolver, but it has a root value that serves as the parent field, and that has a field called hello which is a function. So GraphQL calls the function and then resolves to the value returned by the function.

While you can pass in a resolver through the root for buildSchema , this is not a silver bullet. As it only works for root level fields aka. Query, Mutation or Subscription types. If you wanted to provide a resolver for a field on any other type, there is no way to do it.

Aside from no resolver map, a schema generated using buildSchema:

  • Cannot specify either resolveType or isTypeOf properties for types, making it impossible to use Unions and Interfaces
  • Cannot utilise custom scalars

So buildSchema is not good.

For constructing a GraphQLSchema using GraphQLObject type, it is totally fine, but there is no decoupling between schema and resolver.

We can take a look at the class definition below. So you can provide a GraphQLFieldResolveFn to the resolve field under GraphQLFieldConfig , which is then provided as value to GraphQLFieldConfig and as a list of values up to GraphQLFieldConfigMap , and then fields field on GraphQLObjectTypeConfig

Source code

class GraphQLObjectType {
constructor(config: GraphQLObjectTypeConfig)
}
type GraphQLObjectTypeConfig = {
name: string;
interfaces?: GraphQLInterfacesThunk | Array<GraphQLInterfaceType>;
fields: GraphQLFieldConfigMapThunk | GraphQLFieldConfigMap;
isTypeOf?: (value: any, info?: GraphQLResolveInfo) => boolean;
description?: ?string
}
type GraphQLInterfacesThunk = () => Array<GraphQLInterfaceType>;type GraphQLFieldConfigMapThunk = () => GraphQLFieldConfigMap;type GraphQLFieldConfigMap = {
[fieldName: string]: GraphQLFieldConfig;
};
type GraphQLFieldConfig = {
type: GraphQLOutputType;
args?: GraphQLFieldConfigArgumentMap;
resolve?: GraphQLFieldResolveFn;
deprecationReason?: string;
description?: ?string;
}
type GraphQLFieldResolveFn = (
source?: any,
args?: {[argName: string]: any},
context?: any,
info?: GraphQLResolveInfo
) => any
type GraphQLResolveInfo = {
fieldName: string,
fieldNodes: Array<Field>,
returnType: GraphQLOutputType,
parentType: GraphQLCompositeType,
schema: GraphQLSchema,
fragments: { [fragmentName: string]: FragmentDefinition },
rootValue: any,
operation: OperationDefinition,
variableValues: { [variableName: string]: any },
}
type GraphQLFieldConfigArgumentMap = {
[argName: string]: GraphQLArgumentConfig;
};
type GraphQLArgumentConfig = {
type: GraphQLInputType;
defaultValue?: any;
description?: ?string;
}

As an example:

var AddressType = new GraphQLObjectType({
name: 'Address',
fields: {
street: { type: GraphQLString },
number: { type: GraphQLInt },
formatted: {
type: GraphQLString,
resolve(obj) {
return obj.number + ' ' + obj.street
}
}
}
});

While this is doable, it is not intuitive as the SDL, especially when two types need to refer to each other, or a type needs to refer to itself in a field.

And that’s where the makeExecutableSchema comes in.

makeExecutableSchema

makeExecutableSchema is provided by graphql-tools package that lets you build a schema from SDL and a resolver map object. This is what's used under the hood by apollo-server . makeExecutableSchema constructs a schema from SDL using buildSchema and then mutates the resulting object, adding the resolvers using the addResolversToSchema .

Source code

export function makeExecutableSchema<TContext = any>({
typeDefs,
resolvers = {},
//...
}: IExecutableSchemaDefinition<TContext>) {
... const schemaTransforms: ExecutableSchemaTransformation[] = [
schema => {
const resolverMap: any = Array.isArray(resolvers) ? resolvers.reduce(mergeDeep, {}) : resolvers;

const schemaWithResolvers = addResolversToSchema({
schema,
resolvers: resolverMap,
resolverValidationOptions,
inheritResolversFromInterfaces,
updateResolversInPlace,
});


return schemaWithResolvers;
},
];

....
}

As you can see, we first have our schema, and then get a schemaWithResolvers through addResolversToSchema . So you can simply do:

const typeDefs = `
type Query {
hello: String
}
`
const resolvers = {
Query: {
hello: () => 'Hello!',
},
}
const schema = makeExecutableSchema({ typeDefs, resolvers })

Comparing to buildSchema , you can not only provide a resolvermap, but can provide resolveType properties for your Interfaces or Unions. You can also implement custom scalars and schema directives easily.

const resolvers = {
Query: {
animals: () => getAnimalsFromDB(),
}
Animal: {
__resolveType: (obj) => obj.constructor.name //e.g. Dog
},
Dog: {
owner: (dog) => getDogFromDB(dog.id),
}
}

makeExecutableSchema takes a single argument: an object of options. Only the typeDefs option is required.

const { makeExecutableSchema } = require('apollo-server');const newSchema = makeExecutableSchema({
typeDefs,
resolvers, // optional
logger, // optional
allowUndefinedInResolve = false, // optional
resolverValidationOptions = {}, // optional
directiveResolvers = null, // optional
schemaDirectives = null, // optional
parseOptions = {}, // optional
inheritResolversFromInterfaces = false // optional
});

Note that the typeDefs is a required argument and should be a parsed DocumentNode object (which is what the gql tag produces), or they can be a string, in which case they will be parsed for you.

gql definitionfunction gql(
literals: ReadonlyArray<string> | Readonly<string>,
...placeholders: any[]
): import("graphql").DocumentNode;
====================import gql from "graphql-tag";const typeDefs = gql`scalar Datetype Animal {
id: ID!
adopted: Date
}
`
const server = new ApolloServer({
typeDefs,
// resolvers,
})

That’s it!

Hopefully this clears some of your confusions or at least doesn’t make it worse!

Happy Reading!

--

--