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 or isTypeOf properties for Types, making it impossible to use Unions and Interfaces
  • 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.