Notable differences between buildSchema and GraphQLSchema?
Are there any notable differences between the two? Im interested in anything from runtime and startup performance to features and workflow differences. Documentation does a poor job on explaining the difference and when I should use one over the other.
Example in both versions:
buildSchema
const { 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);
});
GraphQLSchema
const { 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);
});
The buildSchema
function takes a schema in SDL (schema definition language) and returns a GraphQLSchema
object. Given two identical schemas generated with each method, runtime performance would be the same. Startup time for a server using buildSchema
would be slower since parsing the SDL adds an extra step that would not otherwise exist -- whether there would be a noticeable difference, I can't say definitively.
Using buildSchema
is generally inadvisable, as it severely limits the functionality of your schema.
A schema generated using buildSchema
:
- Cannot specify resolve functions for individual fields
- Cannot specify either
resolveType
orisTypeOf
properties for Types, making it impossible to useUnions
andInterfaces
- Cannot utilize custom scalars
Item #1 cannot be stressed enough -- buildSchema
does not allow you to specify a resolver function for any field in your schema. This includes fields on your Query
and Mutation
types. Examples that use buildSchema
get around this problem by relying on GraphQL's default resolver behavior and passing in a root
value.
By default, if a field does not have a resolve
function specified, 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 name of the field. If it finds a match, it resolves the field to that value. If the match happens to be a function, it calls that function first and then resolves to the value returned by the function.
In the example above, the hello
field in the first schema does not have a resolver. GraphQL looks at the parent value, which for root level fields is the root value that is passed in. The root value has a field called hello
, and it's a function, so it calls the function and then resolves to the value returned by the function. You could achieve the same effect simply by making the hello
property a String instead of a function as well.
Given the above, the two examples in the question are actually not the same. Rather, we would have to modify the second schema like this for it to be equivalent:
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: () => ({
hello: {
type: GraphQLString,
}
})
})
});
const root = { hello: () => 'Hello world!' };
graphql(schema, '{ hello }', root).then((response) => {
console.log(response);
});
While passing in a resolver through the root is a neat trick, again it only works for root level fields (like fields on the Query
, Mutation
or Subscription
types). If you wanted to provide a resolver for a field on a different type, there is no way to do so using buildSchema
.
Bottom line: don't use buildSchema
.
But I wanted to use SDL!
And you still can! But... don't do it using vanilla GraphQL.js. Instead, if you want to utilize SDL to generate your schema, you should instead either use graphql-tools
' makeExecutableSchema
or use a more complete solution like apollo-server
, which uses makeExecutableSchema
under the hood. makeExecutableSchema
allows you to define a schema using SDL, while also providing a separate resolvers
object. So you can do:
const typeDefs = `
type Query {
hello: String
}
`
const resolvers = {
Query: {
hello: () => 'Hello!',
},
}
const schema = makeExecutableSchema({ typeDefs, resolvers })
The difference is that, unlike buildSchema
, you can also provide resolvers for other types, and even provide resolveType
properties for your Interfaces or Unions.
const resolvers = {
Query: {
animals: () => getAnimalsFromDB(),
}
Animal: {
__resolveType: (obj) => obj.constructor.name
},
Cat: {
owner: (cat) => getOwnerFromDB(cat.ownerId),
}
}
Using makeExecutableSchema
, you can also implement custom scalars and schema directives, easily customize a variety of schema validation rules and even allow implementing types to inherit resolvers from their interfaces. While it's critical to understand the basics of GraphQL.js and how to generate a basic schema using the GraphQLSchema
constructor, makeExecutableSchema
is a more complete and flexible solution that should be the go-to choice for most projects. See the docs for more details.
UPDATE
If you're bent on using buildSchema
, it's actually possible to get around the inability to provide resolvers for non-root types by using ES6 classes. Check out this sample schema. This doesn't address all the other limitations of buildSchema
, but it does make it more palatable.