@jpwilliams/graphql-modular-loader v0.1.0
@jpwilliams/graphql-modular-loader
Organise and load all of your GraphQL types, resolvers, loaders, and middleware with a single, easy function. This is an opinionated loader which supports only a specific export structure. Like anything opinionated, it's good for standardising, but it's not for everybody.
npm install --save @jpwilliams/graphql-modular-loader// Load the './types' folder
const { loader } = require('@jpwilliams/graphql-modular-loader')
const { typeDefs, resolvers, getContextFns } = loader('./types')
// Use typeDefs and resolvers for your GraphQL server
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
// getContextFns can be used to populate your context
// object with loaders and middleware specified throughout
// your types
// ...
const context = {
userId: 123,
database: MyDbConnection
}
Object.assign(context, getContextFns(context))
// ...An example folder with your entire GraphQL set-up:

How it works
The package will load and parse folders and files as an object, meaning schemas and resolvers can be specified either as individual files, or as a single file with multiple exports. We'll show a valid single file compared to a valid directory structure later, but for now let's look at how things might be laid out in our tree.
Anything that exports multiple values can be loaded as a folder instead. For example:
// foo.js
module.exports.bar = 'bar'
module.exports.baz = 'bazIs exactly the same as:
// foo/bar.js
module.exports = 'bar'// foo/baz.js
module.exports = 'baz'So how does the library expect you to lay out your schemas and resolvers? In their most separated form:
schema.graphqlExports a GraphQL schema for the type you're creating.resolvers/Contains any field resolvers for this type.author.jsExports a function that receives(obj, args, context, info)and returns the value for anauthorfield.
Query/Contains any queries related to this type.books/Contains the schema and resolver for thebooksquery.schema.graphqlExports a GraphQL schema for thebooksquery.resolver.jsExports a function that receives(obj, args, context, info)and handles abooksquery.
Mutation/Contains any mutations related to this type.addBook/Contains the schema and resolver for theaddBookmutation.schema.graphqlExports a GraphQL schema for theaddBookmutation.resolver.jsExports a function that receives(obj, args, context, info)and handles anaddBookmutation.
Subscription/Contains any subscriptions related to this type.bookAdded/Contains the schema and resolver for thebookAddedsubscription.schema.graphqlExports a GraphQL schema for thebookAddedsubscription.resolver.jsExports a function that receives(obj, args, context, info)and handles abookAddedsubscription.
loaders/Contains any loaders related to this type. A specific export is recommended here so that loaders can be accessed from thecontextobject of any resolver.bookByName.jsAdds a loader namedbookByName. Export a function which receives acontextobject and passes back aDataLoaderinstance. The use of a wrapping function here allows loaders to usecontext, but also means you can combatdataloader's caching trap by returning a new loader on each run.
middleware/Contains any middleware related to this type. A specific export is recommended here so that middleware can be accessed from thecontextobject of any resolver.hasAccessToBook.jsAdds a piece of middleware namedhasAccessToBook. Export a function which receives acontextobject and passes back a function to run when called.
Using loaders and middleware with context
Regarding loaders and middleware, here's an example of what a loader called bookByName.js would ideally look like:
// bookByName.js
const DataLoader = require('dataloader')
module.exports = (context) => new DataLoader(async (bookNames) => {
const books = await context.db.pseudoGetBooks(bookNames)
const bookMap = books.reduce((map, book) => {
map[book.name] = book
return map
}, {})
return bookNames.map(bookName => bookMap[bookName])
})And here might be our middleware, hasAccessToBook.js:
// hasAccessToBook.js
module.exports = (context) => async (bookId) => {
if (!context.user.isReader) {
throw new Error('Must be a reader to see that book!')
}
}When loading types, getContextFns function is exported too. This takes a context object and loads all loaders and middleware using that context. Along with that and using something like apollographql/apollo-server, we can add this (and any other loaders/middleware with the same format) to the context object for every resolver like so:
const { loader } = require('@jpwilliams/graphql-modular-loader')
const { ApolloServer } = require('apollo-server')
const { typeDefs, resolvers, getContextFns } = loader('./types')
const server = new ApolloServer({
typeDefs,
resolvers
context: async ({ req }) => {
// set up some basic context here.
// maybe set up DB connections or get user data from the req.
const context = {
foo: 'bar',
baz: true,
dbConnection: '...'
}
Object.assign(context, getContextFns(context))
return context
}
})Now, a resolver could access any loaders or middleware from our context object!
module.exports = async ({ bookName }, args, context, info) => {
await context.middleware.hasAccessToBook(bookName)
return context.loaders.bookByName(bookName)
}Splitting files
So all of this means we can now split up complex types in to nicely separated, bookmarked code, allowing really easy extensibility.
// types/Book/schema.graphql
type Book {
title: String
author: Author
}// types/Book/Query/books/schema.graphql
extend type Query {
books: [Book]
}// types/Book/Query/books/resolver.js
module.exports = (obj, args, context, info) => [{
title: 'Jurassic Park',
author: {name: 'Michael Crichton'}
}]// types/Book/Mutation/addBook/schema.graphql
extend type Mutation {
addBook(input: AddBookInput!): AddBookOutput
}
input AddBookInput {
title: String!
author: String!
}
type AddBookOutput {
book: Book
}// types/Book/Mutation/addBook/resolver.js
module.exports = (obj, args, context, info) => psuedoAddBook(input.title, input.author)// types/Book/Subscription/bookAdded/schema.graphql
extend type Subscription {
bookAdded: BookAddedPayload
}
type BookAddedPayload {
book: Book
}// types/Book/Subscription/bookAdded/resolver.js
module.exports = (obj, args, context, info) => psuedoAsyncIterator('bookAdded')// types/Book/resolvers/author.js
module.exports = (obj, args, context, info) => psuedoGetAuthorData()You could also define this entire type in a single file. It'd work just fine with the package, but could get pretty bloated the more you add to it! This is best for very simple types like imported scalars.
// types/Book.js
const schema = `
type Book {
title: String
author: Author
}
`
const Query = {
books: {
schema: `extend type Query {
books: [Book]
}`,
resolver: (obj, args, context, info) => [{
title: 'Jurassic Park',
author: {name: 'Michael Crichton'}
}]
}
}
const Mutation = {
addBook: {
schema: `extend type Mutation {
addBook(input: AddBookInput!): AddBookOutput
}
input AddBookInput {
title: String!
author: String!
}
type AddBookOutput {
book: Book
}`,
resolver: (obj, args, context, info) => psuedoAddBook(input.title, input.author)
}
}
const Subscription = {
bookAdded: {
schema: `extend type Subscription {
bookAdded: BookAddedPayload
}
type BookAddedPayload {
book: Book
}`,
resolver: (obj, args, context, info) => psuedoAsyncIterator('bookAdded')
}
}
const resolvers = {
author: (obj, args, context, info) => psuedoGetAuthorData()
}
const loaders = {
bookByName: () => new PsuedoDataLoader()
}
module.exports = {
schema,
Query,
Mutation,
Subscription,
resolvers,
loaders
}