apollo-server/docs/source/best-practices/organization.md

6.6 KiB
Raw Blame History

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 cant 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 fieldin 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)
  }
}