How to Build Schema in GraphQL
A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend
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 usesGraphQLObjectType
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
orisTypeOf
properties for types, making it impossible to useUnions
andInterfaces
- 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
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
) => anytype 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
.
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!