This package is supplementary to my GraphQL an Introduction presentation which can be found here.
npm install
npm run start
Once the server is running it will log the url's that you can visit to try out GraphQL.
Queries in GraphQL are used to get state. This example query returns the amount of users that are currently on a list stored in the server.
query {
userCount
}
Mutations are used to modify the state. This mutations allows a user to add a new email to the mailing list that is stored on the server.
mutation {
addEmail (email: "[email protected]")
}
In GraphQL subscriptions are used to watch the state of the server. For this demo, whenever a new email is added to the list, the mailing list is then pushed to the subscription and back to the user running it.
subscription {
mailingList
}
// src/query.js
const query = new GraphQLObjectType({
name: 'Query',
description: 'Base Query Object',
fields: () => ({
mailingListCount: {
type: GraphQLInt,
description: 'The current amount of users in the mailing list.',
resolve: async (_source, _args, { mailingList }) => {
return mailingList.length
},
},
}),
})
In query.js we create a very simple query that best represents the purposes of queries which is allowing the user to get the current state. We do this here by returning the current amount of users that are stored in the mailing list.
// src/mutations.js
const mutation = new GraphQLObjectType({
name: 'Mutation',
description: 'Base Mutation Object',
fields: () => ({
addEmail: {
type: GraphQLString,
description: 'Add new user to a mailing list.',
args: {
email: {
type: new GraphQLNonNull(Email),
description: "User's email.",
},
},
resolve: async (
_source,
{ email },
{ pubsub, PUBSUB_STRING, mailingList },
) => {
mailingList.push(email)
pubsub.publish(PUBSUB_STRING, { mailingList })
return `User: ${email} was successfully added to mailing list.`
},
},
}),
})
In mutation.js we create a mutation that allows us to change the state of the server by adding a new email to the list, we then return a string informing the user that the email was added to the list. We also push the updated mailing list to the subscription, which will be covered below.
// src/subscription.js
const subscription = new GraphQLObjectType({
name: 'Subscription',
description: 'Base Subscription',
fields: () => ({
mailingList: {
type: new GraphQLList(Email),
description: 'Push all emails in mailing list, when a new one is added.',
resolve: async ({ mailingList }) => {
return mailingList
},
subscribe: (_source, _args, { pubsub, PUBSUB_STRING }) =>
pubsub.asyncIterator(PUBSUB_STRING),
},
}),
})
In subscription.js
we use an asyncIterator
to setup the subscription to listen on a certain channel defined in the
entry-point. Whenever a mutation is ran it pushes the updated mailing list to any
user currently listening to the subscription.
// src/scalar.js
const validate = (value) => {
const EMAIL_REGEX = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
if (typeof value !== typeof 'string') {
throw new TypeError(`Value is not a string: ${typeof value}`)
}
if (!EMAIL_REGEX.test(value)) {
throw new TypeError(`Value is not a valid email: ${value}`)
}
return value
}
module.exports.Email = new GraphQLScalarType({
name: 'Email',
description: 'String that conforms to an email structure.',
serialize: validate,
parseValue: validate,
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new GraphQLError(
`Can only validate strings as emails but got a: ${ast.kind}`,
)
}
return validate(ast.value)
},
})
In scalar.js we can define a custom scalar type that we can use throughout our API. In this demo we are creating a mailing list so we need a type that enforces that we can only input emails. Creating this custom email scalar allows us to reject any bad or malicious inputs before they reach the business logic.
const { query } = require('./query')
const { mutation } = require('./mutation')
const { subscription } = require('./subscription')
const Server = (port, context = {}) => {
const app = express()
const server = new ApolloServer({
schema: new GraphQLSchema({
query: query,
mutation: mutation,
subscription: subscription,
}),
context: ({ req, res }) => {
return {
req,
res,
...context,
}
},
})
server.applyMiddleware({ app })
const httpServer = createServer(app)
server.installSubscriptionHandlers(httpServer)
console.log(`π Server ready at http://localhost:${port}${server.graphqlPath}`)
console.log(
`π Subscriptions ready at ws://localhost:${port}${server.subscriptionsPath}`,
)
return httpServer
}
We create a factory function in server.js
that we then use in the entry-point below. We first have to bring in the root objects that we created previously
to build our schema. The factory function takes in a port, and the context that gets passed in from the entry-point.
Having the context field as an anonymous function allows it to be re-created upon each request, this allows us to
gather the request and response express objects that we could use. We have to apply the middleware to the server
which allows us to serve our ApolloServer
, to add WebSockets for the subscriptions we need to create a server
on the express app, we then install the required subscription handlers and return the corresponding httpServer.
const PORT = 3000
const PUBSUB_STRING = 'allUsersPubSub'
const userList = ['John', 'Jane']
const { Server } = require('./src/server')
;(async () => {
const pubsub = new PubSub()
Server(PORT, { pubsub, PUBSUB_STRING, userList }).listen(PORT, (err) => {
if (err) throw err
})
})()
index.js is the entry point in our application. We define our constants used through out this demo, and pass them into the context for use through out the server. We use the factory function return value of the server to setup the server and listen on the specified port.