Graphql-tools: What is the best practice for modularizing GraphQL schema?

Created on 24 Apr 2018  Â·  8Comments  Â·  Source: ardatan/graphql-tools

These days I am looking for the best practice to split schemas into files. My thought is:

  • one file one schema
  • each schema do its own things

@stubailo told me a new, simpler approach here: https://dev-blog.apollodata.com/modularizing-your-graphql-schema-code-d7f71d5ed5f2, which uses the type extension syntax:

type Query {
  _empty: String
}

extend type Query {
  books: [Book]
}

But I have another way to discuss here, which is more simpler and semantic.

Convention

I assume each schema file can export 3 properties(not necessary): schemas, resolvers, directives:

// author.js

// both Array and String are acceptable
export const schemas = [`
  directive @auth(
    requires: AuthRole = ADMIN
  ) on FIELD_DEFINITION | OBJECT

  type Author {
    name: String
    age: Int
  }

  type Query {
    author(name: String!): Author @auth(requires: USER)
    authors: [Author]
  }
`];

export const resolvers = {
  Query: {
    author(source, args, ctx) {
    }
  },
  Author: {
  }
};

export const directives = {
  auth: AuthDirective
};

Here I put the directive schema and the type schema together for simplicity. In production, I suggest to split directive schema, type schema and scalar schema into multiple files.

Merge

The schema definitions above are pretty simple and semantic:A schema has its type definition or directive declaring and resolvers in its own schema scope. Maybe the schema also has dependencies, but are not in its scope.

Now we need a way to merge those schemas, resolvers and directives.

I would like to write a function to load the files together:

const typeDefs = [];
const resolverMap = {};
const directiveMap = {};

const files = loadAllFiles('/path/to/app/graphql');
files.forEach(file => {
   const { schemas, resolvers, directives } = require(file);
   if (!!schemas) {
      typeDefs.push(schemas);
    }
    if (!!resolvers) {
      merge(resolverMap, resolvers);
    }
    if (!!directives) {
      merge(directiveMap, directives);
    }
 });

Then maybe we could call the api makeExecutableSchema to get a root schema with resolvers, but it will throw error shows "type Query defined more than once".

That is because makeExecutableSchema will not merge the multiply defined schemas by the same type name.

__Here I hope the this feature would be implemented later__. I have opened an issue here #708 for this feature.

Finally I find a npm module merge-graphql-schemas which could smartly merge the multiply defined schemas:

import { mergeTypes } from 'merge-graphql-schemas';

makeExecutableSchema({
  typeDefs: mergeTypes(flatten(typeDefs)),
  resolvers: resolverMap,
  schemaDirectives: directiveMap
});

We call flatten here in case the item of typeDefs is an Array.

Now, we've got an Merged Root Schema.

question

Most helpful comment

Full Example Graphql, Mongo, Express, Apollo-Server
https://github.com/hajocava/Graphql-Template

All 8 comments

I am also thinking about this topic recently, what bothers me more is remote schemas. makeExecutableSchema seems not work cause it needs to separate typeDefs and resolvers, so I change to use mergeSchemas, but can not handle errors very well, something subquery fail will return a nested error, and something subquery fails will return a root error.

Recently I was trying graphql and before I discovered other packages, I made a similar thing to load schemas from files, here the code with an example.

The interface that define the one-file graphql exports

interface GraphQLExport {
  inputs?: String
  types?: String
  queries?: String
  mutations?: String
  subscriptions?: String
  resolvers?: Object
}

An example Message.js schema export

const types = `
  type Message {
    id: ID
    chat: ID!
    message: String!
    author: User!
    timestamp: String!
  }
`

const queries = `
  message(id: ID!): Message
  messages(chat: ID): [Message]
`

const mutations = `
  addMessage(message: MessageInput!): Message!
  removeMessage(id: ID!): Message!
`

const subscriptions = `
  onMessage(chat: ID!): Message
`

const resolvers = {
  Query: {
    message: async (_, { id }, { Models }) => {},
    messages: async (_, args, { Models }) => {}
  }
}

export {
  types,
  queries,
  mutations,
  subscriptions,
  resolvers
}

The schema build file, which takes all the partials and merge them in a single schema

// Import schemas
import * as Message from './Message'

const partials: Array<GraphQLExport> = [
  Message
]

const inputs = []
const types = []
const queries = []
const mutations = []
const subscriptions = []

const merge = (target, source) => {
  for (let key of Object.keys(source)) {
    if (source[key] instanceof Object && target[key]) {
      Object.assign(source[key], merge(target[key], source[key]))
    }
  }
  Object.assign(target || {}, source)
  return target
}

// Concat all partial strings
partials.forEach(p => {
  p.inputs && inputs.push(p.inputs)
  p.types && types.push(p.types)
  p.queries && queries.push(p.queries)
  p.mutations && mutations.push(p.mutations)
  p.subscriptions && subscriptions.push(p.subscriptions)
})

const typeDefs = `
  ${inputs.join('\n')}
  ${types.join('\n')}

  ${queries.length ? `
    type Query {
      ${queries.join('\n')}
    }` : ''
  }

  type Mutation {
    ${mutations.join('\n')}
  }

  ${subscriptions.length ? `
    type Subscription {
      ${subscriptions.join('\n')}
    }` : ''
  }
`

const resolvers = partials.reduce((obj, p) => {
  return merge(obj, p.resolvers || {})
}, {
  Query: {},
  Mutation: {},
  Subscription: {}
})

export default {
  typeDefs,
  resolvers
}

I wrote it very quickly, I know is not elegant and probably does not handle all cases, but it worked for what I needed.

I'm thinking about switching to a dedicated module such as merge-graphql-schemas but the idea was to define a interface for the schema exports than put all the logic in the file (queries, resolvers, subscriptions, ...) the difference is in case of Query, Mutation, Subscription you just have to export the strings without the full declaration:

So this Query

type Query {
  message(id: ID!): Message
  messages(chat: ID): [Message]
}

in the export file becomes:

const queries = `
  message(id: ID!): Message
  messages(chat: ID): [Message]
`

Any feedback about it?

Full Example Graphql, Mongo, Express, Apollo-Server
https://github.com/hajocava/Graphql-Template

@hajocava excellent work!

Full Example Graphql, Mongo, Express, Apollo-Server
https://github.com/hajocava/Graphql-Template

Link is not working.

Closing, see resources above.

Here is the example
https://github.com/tunchamroeun/graphql-merge.git

Screenshot

Screenshot

Code example in server file

const express = require('express');
const glob = require("glob");
const {graphqlHTTP} = require('express-graphql');
const {makeExecutableSchema, mergeResolvers, mergeTypeDefs} = require('graphql-tools');
const app = express();
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-resolver.js"
let resolvers = glob.sync('graphql/*/*/*-resolver.js')
let registerResolvers = [];
for (const resolver of resolvers){
// add resolvers to array
    registerResolvers = [...registerResolvers, require('./'+resolver),]
}
//iterate through resolvers file in the folder "graphql/folder/folder/whatever*-type.js"
let types = glob.sync('graphql/*/*/*-type.js')
let registerTypes = [];
for (const type of types){
// add types to array
    registerTypes = [...registerTypes, require('./'+type),]
}
//make schema from typeDefs and Resolvers with "graphql-tool package (makeExecutableSchema)"
const schema = makeExecutableSchema({
    typeDefs: mergeTypeDefs(registerTypes),//merge array types
    resolvers: mergeResolvers(registerResolvers,)//merge resolver type
})
// mongodb connection if you prefer mongodb
require('./helpers/connection');
// end mongodb connection
//Make it work with express "express and express-graphql packages"
app.use('/graphql', graphqlHTTP({
    schema: schema,
    graphiql: true,//test your query or mutation on browser (Development Only)
}));
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');
Was this page helpful?
0 / 5 - 0 ratings

Related issues

capaj picture capaj  Â·  4Comments

flippidippi picture flippidippi  Â·  3Comments

ericclemmons picture ericclemmons  Â·  4Comments

benjaminhon picture benjaminhon  Â·  3Comments

brennantaylor picture brennantaylor  Â·  4Comments