6.6 KiB
title | description |
---|---|
Organizing your code | Scaling your Apollo Server from a single file to your entire team |
The GraphQL schema defines the api for Apollo Server, providing the single source of truth between client and server. A complete schema contains type definitions and resolvers. Type definitions are written and documented in the Schema Definition Language(SDL) to define the valid server entry points. Corresponding to one to one with type definition fields, resolvers are functions that retrieve the data described by the type definitions.
To accommodate this tight coupling, type definitions and resolvers should be kept together in the same file. This collocation allows developers to modify fields and resolvers with atomic schema changes without unexpected consequences. At the end to build a complete schema, the type definitions are combined in an array and resolvers are merged together. Throughout all the examples, the resolvers delegate to a data model, as explained in this section.
Note: This schema separation should be done by product or real-world domain, which create natural boundaries that are easier to reason about.
Prerequisites
- essentials/schema for connection between:
- GraphQL Types
- Resolvers
Organizing schema types
With large schemas, defining types in different files and merging them to create the complete schema may become necessary. We accomplish this by importing and exporting schema strings, combining them into arrays as necessary. The following example demonstrates separating the type definitions of this schema found at the end of the page.
// comment.js
const typeDefs = gql`
type Comment {
id: ID!
message: String
author: String
}
`;
export typeDefs;
The Post
includes a reference to Comment
, which is added to the array of type definitions and exported:
// post.js
const typeDefs = gql`
type Post {
id: ID!
title: String
content: String
author: String
comments: [Comment]
}
`;
// Export Post and all dependent types
export typeDefs;
Finally the root Query type, which uses Post, is created and passed to the server instantiation:
// schema.js
const Comment = require('./comment');
const Post = require('./post');
const RootQuery = gql`
type Query {
post(id: ID!): Post
}
`;
const server = new ApolloServer({
typeDefs: [RootQuery, Post.typeDefs, Comment.typeDefs],
resolvers, //defined in next section
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Organizing resolvers
For the type definitions above, we can accomplish the same modularity with resolvers by combining each type's resolvers together with Lodash's merge
or another equivalent. The end of this page contains a complete view of the resolver map.
// comment.js
const CommentModel = require('./models/comment');
const resolvers = {
Comment: {
votes: (parent) => CommentModel.getVotesById(parent.id)
}
};
export resolvers;
The Post
type:
// post.js
const PostModel = require('./models/post');
const resolvers = {
Post: {
comments: (parent) => PostModel.getCommentsById(parent.id)
}
};
export resolvers;
Finally, the Query type's resolvers are merged and the result is passed to the server instantiation:
// schema.js
const { merge } = require('lodash');
const Post = require('./post');
const Comment = require('./comment');
const PostModel = require('./models/post');
// Merge all of the resolver objects together
const resolvers = merge({
Query: {
post: (_, args) => PostModel.getPostById(args.id)
}
}, Post.resolvers, Comment.resolvers);
const server = new ApolloServer({
typeDefs, //defined in previous section
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Extending types
The extend
keyword provides the ability to add fields to existing types. Using extend
is particularly useful in avoiding a large list of fields on root Queries and Mutations.
//schema.js
const bookTypeDefs = gql`
extend type Query {
books: [Book]
}
type Book {
id: ID!
}
`;
// These type definitions are often in a separate file
const authorTypeDefs = gql`
extend type Query {
authors: [Author]
}
type Author {
id: ID!
}
`;
export const typeDefs = [bookTypeDefs, authorTypeDefs]
const {typeDefs, resolvers} = require('./schema');
const rootQuery = gql`
"Query can and must be defined once per schema to be extended"
type Query {
_empty: String
}`;
const server = new ApolloServer({
typeDefs: [RootQuery].concat(typeDefs),
resolvers,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Note: In the current version of GraphQL, you can’t have an empty type even if you intend to extend it later. So we need to make sure the Query type has at least one field — in this case we can add a fake
_empty
field. Hopefully in future versions it will be possible to have an empty type to be extended later.
Documenting a Schema
In addition to modularization, documentation within the SDL enables the schema to be effective as the single source of truth between client and server. GraphQL GUIs have built-in support for displaying docstrings with markdown syntax, such as those found in the following schema.
"""
Description for the type
"""
type MyObjectType {
"""
Description for field
Supports multi-line description
"""
myField: String!
otherField(
"""
Description for argument
"""
arg: Int
)
}
API
Apollo Server pass typeDefs
and resolvers
to the graphql-tools
's makeExecutableSchema
.
TODO point at graphql-tools makeExecutableSchema
api
Example Application Details
Schema
The full type definitions for the first example:
type Comment {
id: ID!
message: String
author: String
votes: Int
}
type Post {
id: ID!
title: String
content: String
author: String
comments: [Comment]
}
type Query {
post(id: ID!): Post
}
Resolvers
The full resolver map for the first example:
const CommentModel = require('./models/comment');
const PostModel = require('./models/post');
const resolvers = {
Comment: {
votes: (parent) => CommentModel.getVotesById(parent.id)
}
Post: {
comments: (parent) => PostModel.getCommentsById(parent.id)
}
Query: {
post: (_, args) => PostModel.getPostById(args.id)
}
}