How to Combine GraphQL Type Definitions Quickly and Easily with Apollo-Server
If you’re adding more types, queries, and mutations to a GraphQL project, you may quickly find your initial type definition file growing in line length and complexity. Instead of housing all of your type definitions in one file, I have found success in breaking these out into different files based on application domain. Once all of your GraphQL components are located in different, smaller files, it becomes much easier for you (and members of your team!) to easily locate, modify, and evolve your schema. In this quick tutorial, I’ll show you how to easily split up your type definitions using base functionality of apollo-server and without the added weight and complexity of packages like graphql-import. At the end of the article, we will take a quick dive into the Apollo Server source code to investigate some of nuances of how the ApolloServer constructor translates different typeDef input types into the GraphQL schema! These may seem like easy concepts to grasp but I found limited documentation about these features and had a blast diving into the source code so wanted to share what I discovered. Let’s do it!
In the beginning stages of a Apollo-Server project, you’ll probably either have your type definitions specified in the file you construct your ApolloServer in or in a single separate file. In order to start breaking those type definitions out, I went ahead and started creating files based on the general types of resources that I was going to be querying and manipulating data for. Here’s what my typeDefs folder (which holds all of my type definitions looked like) in the beginning stages of my application).
|-- createServer.js - resolvers and typeDefs construct ApolloServer
|-- resolvers - folder where all resolver definitions are held
|-- typeDefs- folder with all type definitions
|-- root.js - hold top level mutation and query we will extend
|-- shared.js - holds common types, enums, etc. used across app
Organizations, Users, and Marketing Campaigns are the first important resources that I needed to define the types of so it made since to split them off into their own files. Shared will hold common types to be used across domains (such as date-times and currency) and I will use the Root file to start a base Mutation and Query to extend in all of the other files will all be accessible on the top level Mutation and Query key.
Here is what one of my smaller file for a user resource actually looks like.
I use graphql-tag’s (an Apollo project) gql method to translate a template literal string into a GraphQL AST (this package also allows for some nice syntax highlighting in VSCode) and then simply exported that object from the module. This way, I have all of the resource or domain specific types, queries, and mutations in one place.
It’s important to note that I am extending the Mutation and Query type. I actually have those types defined in my root file which looks like this:
This root Mutation and Query only serve on purpose — to be extended on by the Queries and Mutations in my other files. It’s a current limitation of Apollo-Server that these “base” types require having a Query or Mutation on them. If I remove line 5 and try to create an empty Query type to extend later on, Apollo will throw a syntax error. I’ve read that they will be fixing or improving this functionality in the future so stay tuned for that!
Now, in order to get these typeDefs integrated into my actual schema, I just require in this file and pop my schemaArray array into the typeDefs key on my ApolloServer constructor.
And that’s it! Now I’ve got a cleaner schema, easily split up by application domain that is completely extendable as my application continues to evolve. This schema is completely explorable using GraphQL playground or by hitting the /graphql endpoint directly in the browser.
Lastly, I was curious as to how and why the typeDef key in the ApolloServer constructor could accept both a single typeDef file AND an array of typeDefs.The Apollo Server docs state that you can pass the typeDef key a DocumentNode or an Array of Document Nodes but I simply wanted to understand how this functions under the hood. To get to the bottom of this question, I jumped into the Apollo Server source code.
The code I’m referencing in the following example is pulled from the apollo-server repo version 2.3.1 (so there is a good chance that it may have changed if you are looking at this in the future). After some digging, I located the file that we are interested in apollo-server/packages/apollo-server-core/src/ApolloServer.ts. This file exports the ApolloServerBase class which defines the core behavior of ApolloServer which is then extended and optimized for the target framework (express, koa, connect, hapi).
On line 118 the Constructor function is defined which takes a single parameter of config:
There’s a quick sanity check error thrown if there is somehow no config and then that config is restructured into the different keys defined in the config. TypeDefs are defined on line 126. The typeDef variable is next used on line 239.
TypeDefs are processed after both the schema (a complete schema of typeDefs and resolvers that will override any individual typeDefs or resolvers you pass to the constructor) and module (independent code modules used to generate schemas). Therefore, that check on line 239 for the existence of typeDefs will only throw if there is no schema, modules, OR typeDefs (due to if statement logic elsewhere in the code).
I hope you enjoyed this quick typeDef and deep dive into the apollo-server source code. I am excited to continue evolving my knowledge of GraphQL as I evolve my own applications. Feel free to leave any comments or suggestions below! Thank you!