Building Chatty — Part 2: GraphQL Queries with Express

A WhatsApp clone with React Native and Apollo

Simon Tucker
React Native Training
10 min readApr 9, 2017

--

This is the second blog in a multipart series where we will be building Chatty, a WhatsApp clone, using React Native and Apollo. You can view the code for this part of the series here.

In this section, we will be designing GraphQL Schemas and Queries and connecting them to real data on our server.

Here are the steps we will accomplish in this tutorial:

  1. Build GraphQL Schemas to model User, Group, and Message data types
  2. Design GraphQL Queries for fetching data from our server
  3. Create a basic SQL database with Users, Groups, and Messages
  4. Connect our database to our Apollo Server using Connectors and Resolvers
  5. Test out our new Queries using GraphQL Playground

Designing GraphQL Schemas

GraphQL Type Schemas define the shape of the data our client can expect. Chatty is going to need data models to represent Messages, Users, and Groups at the very least, so we can start by defining those. We’ll update server/data/schema.js to include some basic Schemas for these types:

Step 2.1: Update Schema

Added server/data/schema.js

...import { gql } from 'apollo-server';export const typeDefs = gql`
# declare custom scalars
scalar Date
# a group chat entity
type Group {
id: Int! # unique id for the group
name: String # name of the group
users: [User]! # users in the group
messages: [Message] # messages sent to the group
}
# a user -- keep type really simple for now
type User {
id: Int! # unique id for the user
email: String! # we will also require a unique email per user
username: String # this is the name we'll show other users
messages: [Message] # messages sent by user
groups: [Group] # groups the user belongs to
friends: [User] # user's friends/contacts
}
# a message sent from a user to a group
type Message {
id: Int! # unique id for message
to: Group! # group message was sent in
from: User! # user who sent the message
text: String! # message text
createdAt: Date! # when message was created
}
`;
export default typeDefs;

Changed server/index.js

...import { ApolloServer } from 'apollo-server';
import { typeDefs } from './data/schema';
const PORT = 8080;const server = new ApolloServer({ typeDefs, mocks: true });server.listen({ port: PORT }).then(({ url }) => console.log(`🚀 Server ready at ${url}`));

The GraphQL language for Schemas is pretty straightforward. Keys within a given type have values that are either scalars, like a String, or another type like Group.

Field values can also be lists of types or scalars, like messages in Group.

Any field with an exclamation mark is a required field!

Some notes on the above code:

  • We need to declare the custom Date scalar as it’s not a default scalar in GraphQL. You can learn more about scalar types here.
  • In our model, all types have an id property of scalar type Int!. This will represent their unique id in our database.
  • User type will require a unique email address. We will get into more complex user features such as authentication in a later tutorial.
  • User type does not include a password field. Our client should NEVER need to query for a password, so we shouldn’t expose this field even if it is required on the server. This helps prevent passwords from falling into the wrong hands!
  • Message gets sent “from” a User “to” a Group.

Designing GraphQL Queries

GraphQL Queries specify how clients are allowed to query and retrieve defined types. For example, we can make a GraphQL Query that lets our client ask for a User by providing either the User’s unique id or email address:

type Query {
# Return a user by their email or id
user(email: String, id: Int): User
}

We could also specify that an argument is required by including an exclamation mark:

type Query {
# Return a group by its id -- must supply an id to query
group(id: Int!): Group
}

We can even return an array of results with a query, and mix and match all of the above. For example, we could define a message query that will return all the messages sent by a user if a userId is provided, or all the messages sent to a group if a groupId is provided:

type Query {
# Return messages sent by a user via userId
# ... or ...
# Return messages sent to a group via groupId
messages(groupId: Int, userId: Int): [Message]
}

There are even more cool features available with advanced querying in GraphQL. However, these queries should serve us just fine for now:

Step 2.2: Add Queries to Schema

Changed server/data/schema.js

...    text: String! # message text
createdAt: Date! # when message was created
}
# query for types
type Query {
# Return a user by their email or id
user(email: String, id: Int): User
# Return messages sent by a user via userId
# Return messages sent to a group via groupId
messages(groupId: Int, userId: Int): [Message]
# Return a group by its id
group(id: Int!): Group
}
schema {
query: Query
}
`;
export default typeDefs;

Note that we also need to define the schema at the end of our Schema string.

Connecting Mocked Data

We have defined our Schema including queries, but it’s not connected to any sort of data.

While we could start creating real data right away, it’s good practice to mock data first. Mocking will enable us to catch any obvious errors with our Schema before we start trying to connect real data, and it will also help us down the line with testing. ApolloServer will already naively mock our data with the mock: true setting, but we can also pass in our own advanced mocks.

Let’s create server/data/mocks.js and code up some mocks with faker (npm i faker) to produce some fake data:

Step 2.3: Update Mocks

Changed package.json

...  },
"dependencies": {
"apollo-server": "^2.0.0",
"faker": "^4.1.0",
"graphql": "^0.13.2"
}
}

Added server/data/mocks.js

...import faker from 'faker';export const mocks = {
Date: () => new Date(),
Int: () => parseInt(Math.random() * 100, 10),
String: () => 'It works!',
Query: () => ({
user: (root, args) => ({
email: args.email,
messages: [{
from: {
email: args.email,
},
}],
}),
}),
User: () => ({
email: faker.internet.email(),
username: faker.internet.userName(),
}),
Group: () => ({
name: faker.lorem.words(Math.random() * 3),
}),
Message: () => ({
text: faker.lorem.sentences(Math.random() * 3),
}),
};
export default mocks;

Changed server/index.js

...import { ApolloServer } from 'apollo-server';
import { typeDefs } from './data/schema';
import { mocks } from './data/mocks';
const PORT = 8080;const server = new ApolloServer({ typeDefs, mocks });server.listen({ port: PORT }).then(({ url }) => console.log(`🚀 Server ready at ${url}`));

While the mocks for User, Group, and Message are pretty simple looking, they’re actually quite powerful. If we run a query in GraphQL Playground, we'll receive fully populated results with backfilled properties, including example list results. Also, by adding details to our mocks for the user query, we ensure that the email field for the User and from field for their messages match the query parameter for email:

Sounds like Jabba to me…

Connecting Real Data

Let’s connect our Schema to some real data now. We’re going to start small with a SQLite database and use the sequelize ORM to interact with our data.

npm i sqlite3 sequelize
npm i lodash # top notch utility library for handling data
npm i graphql-date # graphql custom date scalar

First we will create tables to represent our models. Next, we’ll need to expose functions to connect our models to our Schema. These exposed functions are known as Connectors. We’ll write this code in a new file server/data/connectors.js:

Step 2.4: Create Connectors

Changed package.json

...  "dependencies": {
"apollo-server": "^2.0.0",
"faker": "^4.1.0",
"graphql": "^0.13.2",
"lodash": "^4.17.4",
"sequelize": "^4.4.2",
"sqlite3": "^4.0.1"
}
}

Added server/data/connectors.js

...import Sequelize from 'sequelize';// initialize our database
const db = new Sequelize('chatty', null, null, {
dialect: 'sqlite',
storage: './chatty.sqlite',
logging: false, // mark this true if you want to see logs
});
// define groups
const GroupModel = db.define('group', {
name: { type: Sequelize.STRING },
});
// define messages
const MessageModel = db.define('message', {
text: { type: Sequelize.STRING },
});
// define users
const UserModel = db.define('user', {
email: { type: Sequelize.STRING },
username: { type: Sequelize.STRING },
password: { type: Sequelize.STRING },
});
// users belong to multiple groups
UserModel.belongsToMany(GroupModel, { through: 'GroupUser' });
// users belong to multiple users as friends
UserModel.belongsToMany(UserModel, { through: 'Friends', as: 'friends' });
// messages are sent from users
MessageModel.belongsTo(UserModel);
// messages are sent to groups
MessageModel.belongsTo(GroupModel);
// groups have multiple users
GroupModel.belongsToMany(UserModel, { through: 'GroupUser' });
const Group = db.models.group;
const Message = db.models.message;
const User = db.models.user;
export { Group, Message, User };

Let’s also add some seed data so we can test our setup right away. The code below will add 4 Groups, with 5 unique users per group, and 5 messages per user within that group:

Step 2.5: Create fake users

Changed server/data/connectors.js

...import { _ } from 'lodash';
import faker from 'faker';
import Sequelize from 'sequelize';
// initialize our database...// groups have multiple users
GroupModel.belongsToMany(UserModel, { through: 'GroupUser' });
// create fake starter data
const GROUPS = 4;
const USERS_PER_GROUP = 5;
const MESSAGES_PER_USER = 5;
faker.seed(123); // get consistent data every time we reload app
// you don't need to stare at this code too hard
// just trust that it fakes a bunch of groups, users, and messages
db.sync({ force: true }).then(() => _.times(GROUPS, () => GroupModel.create({
name: faker.lorem.words(3),
}).then(group => _.times(USERS_PER_GROUP, () => {
const password = faker.internet.password();
return group.createUser({
email: faker.internet.email(),
username: faker.internet.userName(),
password,
}).then((user) => {
console.log(
'{email, username, password}',
`{${user.email}, ${user.username}, ${password}}`
);
_.times(MESSAGES_PER_USER, () => MessageModel.create({
userId: user.id,
groupId: group.id,
text: faker.lorem.sentences(3),
}));
return user;
});
})).then((userPromises) => {
// make users friends with all users in the group
Promise.all(userPromises).then((users) => {
_.each(users, (current, i) => {
_.each(users, (user, j) => {
if (i !== j) {
current.addFriend(user);
}
});
});
});
})));
const Group = db.models.group;
const Message = db.models.message;
const User = db.models.user;

For the final step, we need to connect our Schema to our Connectors so our server resolves the right data based on the request. We accomplish this last step with the help of Resolvers.

In ApolloServer, we write Resolvers as a map that resolves each GraphQL Type defined in our Schema. For example, if we were just resolving User, our resolver code would look like this:

// server/data/resolvers.js
import { User, Message } from './connectors';
export const resolvers = {
Query: {
user(_, {id, email}) {
return User.findOne({ where: {id, email}});
},
},
User: {
messages(user) {
return Message.findAll({
where: { userId: user.id },
});
},
groups(user) {
return user.getGroups();
},
friends(user) {
return user.getFriends();
},
},
};
export default resolvers;

When the user query is executed, it will return the User in our SQL database that matches the query. But what’s really cool is that all fields associated with the User will also get resolved when they're requested, and those fields can recursively resolve using the same resolvers. For example, if we requested a User, her friends, and her friend’s friends, the query would run the friends resolver on the User, and then run friends again on each User returned by the first call:

user(id: 1) {
username # the user we queried
friends { # a list of their friends
username
friends { # a list of each friend's friends
username
}
}
}

This is extremely cool and powerful code because it allows us to write resolvers for each type just once, and have it work anywhere and everywhere!

So let’s put together resolvers for our full Schema in server/data/resolvers.js:

Step 2.6: Create Resolvers

Changed package.json

...    "apollo-server": "^2.0.0",
"faker": "^4.1.0",
"graphql": "^0.13.2",
"graphql-date": "^1.0.3",
"lodash": "^4.17.4",
"sequelize": "^4.4.2",
"sqlite3": "^4.0.1"

Added server/data/resolvers.js

...import GraphQLDate from 'graphql-date';import { Group, Message, User } from './connectors';export const resolvers = {
Date: GraphQLDate,
Query: {
group(_, args) {
return Group.find({ where: args });
},
messages(_, args) {
return Message.findAll({
where: args,
order: [['createdAt', 'DESC']],
});
},
user(_, args) {
return User.findOne({ where: args });
},
},
Group: {
users(group) {
return group.getUsers();
},
messages(group) {
return Message.findAll({
where: { groupId: group.id },
order: [['createdAt', 'DESC']],
});
},
},
Message: {
to(message) {
return message.getGroup();
},
from(message) {
return message.getUser();
},
},
User: {
messages(user) {
return Message.findAll({
where: { userId: user.id },
order: [['createdAt', 'DESC']],
});
},
groups(user) {
return user.getGroups();
},
friends(user) {
return user.getFriends();
},
},
};
export default resolvers;

Our resolvers are relatively straightforward. We’ve set our message resolvers to return in descending order by date created, so the most recent messages will return first.

Notice we’ve also included a resolver for Date because it's a custom scalar. Instead of creating our own resolver, I’ve imported someone’s excellent GraphQLDate package.

Finally, we can pass our resolvers to ApolloServer in server/index.js to replace our mocked data with real data:

Step 2.7: Connect Resolvers to GraphQL Server

Changed .gitignore

...node_modules
npm-debug.log
yarn-error.log
.vscode
chatty.sqlite

Changed server/index.js

...import { ApolloServer } from 'apollo-server';
import { typeDefs } from './data/schema';
import { mocks } from './data/mocks';
import { resolvers } from './data/resolvers';
const PORT = 8080;const server = new ApolloServer({
resolvers,
typeDefs,
// mocks,
});
server.listen({ port: PORT }).then(({ url }) => console.log(`🚀 Server ready at ${url}`));

Now if we run a Query in GraphQL Playground, we should get some real results straight from our database:

KaBlaM!

We’ve got the data. We’ve designed the Schema with Queries. Now it’s time to put that data in our React Native app!

In the next part of this series, we will design the layout for our React Native app and fill it with data from our server using the Schema we just built.

As always, please share your thoughts, questions, struggles, and breakthroughs below!

You can view the code for this tutorial here

Continue to Building Chatty — Part 3 (GraphQL Queries with React-Apollo)

--

--