A Simple GraphQL Example with Relationships

If you're trying to build a simple GraphQL example using Node.js to teach yourself the technology and you've been frustrated with other tutorials then you've come to the right place.
This tutorial focuses less on the setup of the server and more on how you actually structure your schema and resolvers which is the hard part to wrap your head around without clear examples.
Feel free to use my accompanying repo as an interactive reference while reading this article tylerbuchea/graphql-apollo-example.
Real World Example with Relationships
I am using apollo-server
there are other options such as express-graphql
but I prefer Apollo. The syntax is very similar and as long as you're not using any of Apollo's more "advanced" features you would be able to switch between the two in a very short period if you wanted to.
Below I'm going to lay out a simple Schema and Resolver setup that hits on all the main concepts of GraphQL so you'll be able to mimic those patterns in your own application and actually build something that can be used by a modern day app. The main piece most tutorials are missing is an example of querying for nested relationships which is what I'm going to demonstrate below. You should be able to extrapolate any number of relations after seeing a basic and realistic example of one.
const { ApolloServer, gql } = require('apollo-server');
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'makethislongandrandom';
// This could also be MongoDB, PostgreSQL, etc
const db = {
users: [
{
organization: '123', // this is a relation by id
id: 'abc',
name: 'Elon Musk',
},
],
organizations: [
{
users: ['abc'], // this is a relation by ids
id: '123',
name: 'Space X',
},
],
};
// All the code needed for a working GraphQL API
// context, typeDefs (schema), and resolvers
const server = new ApolloServer({
context: ({ req }) => {
let user = null;
try {
const token = req.headers.authorization.replace('Bearer ', '');
user = jwt.verify(token, JWT_SECRET);
} catch (error) {}
return { user };
},
typeDefs: gql`
type Mutation {
signup(organization: String, id: String, name: String): User
}
type Query {
login(username: String): String
tellMeADadJoke: String
users: [User]
user(id: ID!): User
organizations: [Organization]
organization(id: ID!): Organization
}
type User {
organization: Organization
id: ID
name: String
}
type Organization {
users: [User]
id: ID
name: String
}
`,
resolvers: {
Mutation: {
signup(_, { organization, id, name }) {
const user = { organization, id, name };
const match = db.users.find(user => user.name === name);
if (match) throw Error('This username already exists');
db.users.push(user);
return user;
},
},
Query: {
login(_, { username }) {
const user = db.users.find(user => user.name === username);
if (!user) {
throw Error('username was incorrect');
}
const token = jwt.sign({ id: user.id }, JWT_SECRET);
return token;
},
tellMeADadJoke(_, data, { user }) {
if (!user) throw Error('not authorized');
return 'If you see a robbery at an Apple Store does that make you an iWitness?';
},
users: () => db.users,
user: (_, { id }) => db.users.find(user => user.id === id),
organizations: () => db.organizations,
organization: (_, { id }) =>
db.organizations.find(organization => organization.id === id),
},
User: {
organization: parent =>
db.organizations.find(({ id }) => parent.organizationId === id),
},
Organization: {
async users(parent) {
await new Promise(resolve => setTimeout(resolve, 5000));
const organization = db.users.filter(({ id }) =>
parent.userIds.includes(id)
);
return organization;
},
},
},
});
server.listen().then(({ url }) => console.log(`Server ready at ${url}`));
With the structure above you can now perform complex queries that grab only the data you need in the structure you need.
You can try this out by running yarn dev
, visiting http://localhost:4000, and then entering the query below.
query {
organization(id: "123") {
name
users {
name
organization {
name
}
}
}
}
The query above outputs this JSON
{
"data": {
"organization": {
"name": "Space X",
"users": [
{
"name": "Elon Musk",
"organization": {
"name": "Space X"
}
}
]
}
}
}
Schemas and Resolvers
Prefer building a GraphQL schema that describes how clients use the data, rather than mirroring the legacy database schema. - Thinking in Graphs
The schema describes your dataset and types to the client and does some basic type validation. It's an abstraction layer and does not need to match your database one for one. Because sometime pieces of the schema are pulled from multiple sources.
The resolvers map to your schema and are what GraphQL actually executes to retrieve each piece of data. The resolvers are like controllers in a regular REST API.
Relationships
The way you form relationships is by defining custom types in your resolvers:
const resolver = {
// Query resolvers
Query: {
users: () => db.users,
organizations: () => db.organizations,
},
// Custom resolver types
User: {
organization(parent) {
const organization = db.organizations.find(
({ id }) => parent.organizationId === id
);
return organization;
},
},
Organization: {
async users(parent) {
await new Promise(resolve => setTimeout(resolve, 5000));
const organization = db.users.filter(({ id }) =>
parent.userIds.includes(id)
);
return organization;
},
},
}
GraphQL will first get the data returned from the // Query resolvers
and then check if any of the fields you queried are defined in a // Customer resolver types
, execute those and return that data along with the data returned from the first resolver. You can think of it as a merge
or extends
type of operation. Apollo conveniently passes along the parent objects data in case you need it to query for the relationship.
GraphQL allows you to connect data from multiple sources. In your Query resolver
you could grab the "organizations" from MongoDB and then in the Customer resolver type
- Organization.user()
method you could get the the users associated for those entries from an entirely different database or even a REST endpoint.
Async
I know what you're thinking, "This is great for synchronous data fetching from a JavaScript object but what about when I want to call into my database or to a REST API?" Well I wouldn't leave you hanging! The answer is simple GraphQL can also handle promises and will resolve accordingly so you can use async methods as well.
Replace your custom Organization
resolver with the one below then run the query again. It should take five seconds to load this time because it is async but it will have all the same data.
const resolvers = {
// ...
Organization: {
async users(parent) {
await new Promise(resolve => setTimeout(resolve, 5000));
const organization = db.users.filter(({ id }) =>
parent.userIds.includes(id)
);
return organization;
},
},
}
Mutations
"Okay great", you might be saying, "But how do I mutate data?" Well you have probably heard about mutations in GraphQL and I'm going to let you in on a little secret. Mutations and Queries are exactly the same thing it is only a semantic difference for separating code that changes data from code that simply queries data. So given this knowledge we can easily create mutations with our prior knowledge.
Just add the following code and your ready to go. Just remember nothing is technically stoping you from putting the signup mutation under Query
but it is bad practice by putting it under mutation you're signaling to anyone consuming your API that this operation will change the data.
const typeDefs = gql`
type Mutation {
signup(organization:String, id:String, name:String): User
}
#...
`;
const resolvers = {
Mutation: {
signup(parent, { organization, id, name }) {
const user = { organization, id, name };
db.users.push(user);
return user;
},
},
//...
};
And you consume it just like you would a query except you just change the keyword to mutation
:
mutation {
signup(organization:"123", id:"newUserId", name:"Tyler Buchea") {
name
}
}
Errors
How do we handle errors? You simply throw errors and GraphQL will respond accordingly with a { errors: [...] }
JSON object.
const resolvers = {
Mutation: {
signup(parent, { organization, id, name, password }) {
const user = { id, organization, name, password };
const match = db.users.find(user => user.name === name);
if (match) throw Error('This username already exists');
db.users.push(user);
return user;
},
},
//...
};
Authentication
Another thing I see lacking in most examples is JWT (JSON web token) authentication and how to handle it in a GraphQL server. It's very similar to how you'd do things in a REST API we simply place in a middleware that decodes the incoming request and attaches the decoded data to the request object then instead of operating directly off of the request object we pass it into GraphQL via the context object. Which is just an object that will get passed as the third argument to all of our root resolvers.
const JWT_SECRET = 'makethislongandrandom';
const server = new ApolloServer({
context: ({ req }) => {
let user = null;
try {
const token = req.headers.authorization.replace('Bearer ', '');
user = jwt.verify(token, JWT_SECRET);
} catch (error) {}
return { user };
},
// ...
}
Now let's say we have a super secret query that we only want logged in users to be able to access. Keep in mind that all of our resolvers so far have been completely public. Now that we have a user context we can make our tellMeADadJoke
query safe from the public.
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'makethislongandrandom';
//...
const typeDefs = gql`
type Query {
login: String
tellMeADadJoke: String
#...
}
#...
`;
const resolvers = {
Query: {
login(parent, { username }) {
const user = db.users.find(user => user.name === username);
if (!user) {
throw Error('username was incorrect');
}
const secret = 'makethislongandrandom';
const token = jwt.sign(payload, JWT_SECRET);
return token;
},
tellMeADadJoke(parent, data, { user }) { // this third arg is "context"
if (!user) throw Error('not authorized');
return 'If you see a robbery at an Apple Store does that make you an iWitness?';
},
}
//...
}
You'll have to login first and get yourself a JWT token then apply it to the request to get back the joke you want.
query {
login(username:"Elon Musk")
}
Add the result to your HTTP Headers: { "Authorization": "Bearer TOKEN" }
. Then you can:
query {
tellMeADadJoke
}
Here is a header for you to use with this example so you don't have to hit login first:
{ "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFiYyIsImlhdCI6MTUzMjQ1NDE4MH0.U2IXOLpKqcPLCtzIasl_U8cK_I5tDMAW_CPN5szzhwA" }
Conclusion
This post ended up being much larger than I intended but I hope it gave a more practical view of how to implement the essential aspects of any modern API in GraphQL.
Please let me know if I missed anything in the comments below and thanks for hanging in there with me. 🎉
Resources
- Apollo Server Introduction
- Apollo Server Authentication
- GraphQL buildSchema Example
- GraphQL Object Types
- Zeit - Building a Basic GraphQL Application
- express-graphql issue - nested data example
- Nested query for buildSchema
- Creating A GraphQL Server With Node.js And Express
- StephenGrider/GraphQLCasts
- Thinking in Graphs
- Mutations and Input Types
- GraphQL Tools
- GraphQL Depth Limit
- Merge GraphQL Schemas
- ES Modules demo server
- Mutations and Queries are the same thing
- Mutations and input types
- More GraphQL Concepts
- TypeGraphQL
- Improve doc: graphl-js does not support interfaces and unions with buildSchema