Building Chatty — Part 8: GraphQL Input Types

A WhatsApp clone with React Native and Apollo

Simon Tucker
React Native Training
10 min readSep 26, 2017

--

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

In the first 7 parts of this series, we put together a WhatsApp clone starting from scratch all the way to MVP.

Where we’re at

Steps

  1. Setup
  2. GraphQL Queries with Express
  3. GraphQL Queries with React Apollo
  4. GraphQL Mutations & Optimistic UI
  5. GraphQL Pagination
  6. GraphQL Subscriptions
  7. GraphQL Authentication

In this tutorial, we’ll focus on adding GraphQL Input Types, which will help us clean up our queries and streamline future GraphQL development in our app.

Here’s what we’ll accomplish in this tutorial:

  1. Discuss writing more flexible GraphQL requests
  2. Add GraphQL Input Types to our Schema
  3. Update resolvers and business logic to handle Input Types
  4. Update client-side GraphQL requests to use Input Types

Writing flexible GraphQL

So far in our journey, writing GraphQL queries has been a breeze. We’ve been able to get all the data we need in a single request in exactly the shape we want with very little code. But APIs inevitably get more complex as apps mature. We need to ensure our GraphQL infrastructure adapts gracefully as we expand the functionality of our app.

Let’s do an audit of the GraphQL queries that currently power our React Native client to look for opportunities to improve our querying….

You may notice the queries, mutations, and subscriptions that return Group types have a similar shape, but don't share any code. If we modify or add a field to the Group type later on, we would need to individually update every query and mutation that returns a Group type -- not good.

import gql from 'graphql-tag';import MESSAGE_FRAGMENT from './message.fragment';// this is our primary group query
const GROUP_QUERY = gql`
query group($groupId: Int!, $first: Int, $after: String, $last: Int, $before: String) {
group(id: $groupId) {
id
name
users {
id
username
}
messages(first: $first, after: $after, last: $last, before: $before) {
edges {
cursor
node {
... MessageFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
}
${MESSAGE_FRAGMENT}
`;
// creating a group returns a similar shape for the new group
const CREATE_GROUP_MUTATION = gql`
mutation createGroup($name: String!, $userIds: [Int!]) {
createGroup(name: $name, userIds: $userIds) {
id
name
users {
id
}
messages(first: 1) { # we don't need to use variables
edges {
cursor
node {
... MessageFragment
}
}
}
}
}
${MESSAGE_FRAGMENT}
`;
// unsurprisingly subscriptions to new groups
// looks like CREATE_GROUP_MUTATION
const GROUP_ADDED_SUBSCRIPTION = gql`
subscription onGroupAdded($userId: Int){
groupAdded(userId: $userId){
id
name
messages(first: 1) {
edges {
cursor
node {
... MessageFragment
}
}
}
}
}
${MESSAGE_FRAGMENT}
`;
// and the group field in USER_QUERY looks a lot like these too
export const USER_QUERY = gql`
query user($id: Int) {
user(id: $id) {
id
email
username
groups {
id
name
messages(first: 1) { # we don't need to use variables
edges {
cursor
node {
... MessageFragment
}
}
}
}
friends {
id
username
}
}
}
${MESSAGE_FRAGMENT}
`;

If we create a common GraphQL fragment for our queries and mutations to share, we’ll only need to update the one fragment when the Grouptype changes and all our queries, mutations, and subscriptions will benefit:

Step 8.1: Create GROUP_FRAGMENT

Added client/src/graphql/group.fragment.js

...import gql from 'graphql-tag';import MESSAGE_FRAGMENT from './message.fragment';const GROUP_FRAGMENT = gql`
fragment GroupFragment on Group {
id
name
users {
id
username
}
messages(first: $first, last: $last, before: $before, after: $after) {
edges {
cursor
node {
... MessageFragment
}
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
${MESSAGE_FRAGMENT}
`;
export default GROUP_FRAGMENT;

Now we can update all these GraphQL requests to use the fragment:

Step 8.2: Apply GROUP_FRAGMENT to Queries with default variables

Changed client/src/graphql/create-group.mutation.js

...import gql from 'graphql-tag';import GROUP_FRAGMENT from './group.fragment';const CREATE_GROUP_MUTATION = gql`
mutation createGroup($name: String!, $userIds: [Int!], $first: Int = 1, $after: String, $last: Int, $before: String) {
createGroup(name: $name, userIds: $userIds) {
... GroupFragment
}
}
${GROUP_FRAGMENT}
`;
export default CREATE_GROUP_MUTATION;

Changed client/src/graphql/group-added.subscription.js

...import gql from 'graphql-tag';import GROUP_FRAGMENT from './group.fragment';const GROUP_ADDED_SUBSCRIPTION = gql`
subscription onGroupAdded($userId: Int, $first: Int = 1, $after: String, $last: Int, $before: String){
groupAdded(userId: $userId){
... GroupFragment
}
}
${GROUP_FRAGMENT}
`;
export default GROUP_ADDED_SUBSCRIPTION;

Changed client/src/graphql/group.query.js

...import gql from 'graphql-tag';import GROUP_FRAGMENT from './group.fragment';const GROUP_QUERY = gql`
query group($groupId: Int!, $first: Int = 1, $after: String, $last: Int, $before: String) {
group(id: $groupId) {
... GroupFragment
}
}
${GROUP_FRAGMENT}
`;
export default GROUP_QUERY;

There are a few things worth noting about this pattern:

  1. Changing fields on GROUP_FRAGMENT will immediately apply to all queries, mutations, and subscriptions that use it.
  2. We are occasionally using default values for the $first variable -- $first: Int = 1 to return the first message in a Group if that variable is not specified when executing the query/mutation/subscription.

(GraphQL default variables is without a doubt the greatest and most essential addition to apollo-client of all time, and whoever wrote that PR deserves free beer for life 😉)

  1. Our GraphQL requests have much simpler return shapes, but much more complex sets of variables.

Old CREATE_GROUP_MUTATION:

mutation createGroup($name: String!, $userIds: [Int!]) { ... }

New CREATE_GROUP_MUTATION:

mutation createGroup($name: String!, $userIds: [Int!], $first: Int = 1, $after: String, $last: Int, $before: String) { ... }

Yeesh! If we needed to change a variable used in GROUP_FRAGMENT, we'd still have to change all the queries/mutations/subscriptions. Moreover, it's not very clear what all these variables mean. $first, $after, $last, and $before are variables we use to paginate messages within a Group, but those variables need to be specified in USER_QUERY -- that's nonobvious and weird. What we need is a way to abstract inputs to simplify the way we declare variables and update those variables as our app evolves. Enter GraphQL Input Types!

Input Types on the Server

GraphQL Input Types are a super simple concept — you can declare named arguments in a GraphQL request in whatever shape you want.

For example, we can abstract away the pagination variables from our GraphQL requests by adding the following ConnectionInput in our schema:

# input for relay cursor connections
input ConnectionInput {
first: Int
after: String
last: Int
before: String
}

This will enable us to update Group like so:

# 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(messageConnection: ConnectionInput): MessageConnection # messages sent to the group
}

This will drastically simplify any request that returns Group types!

We should strive to apply input types to all of our GraphQL requests that have even the slightest complexity in their input requirements. For Chatty, I’ve added input types for most of our mutations:

Step 8.3: Add Input Types to Schema

Changed server/data/schema.js

...  # declare custom scalars
scalar Date
# input for creating messages
input CreateMessageInput {
groupId: Int!
text: String!
}
# input for creating groups
input CreateGroupInput {
name: String!
userIds: [Int!]
}
# input for updating groups
input UpdateGroupInput {
id: Int!
name: String
userIds: [Int!]
}
# input for signing in users
input SigninUserInput {
email: String!
password: String!
username: String
}
# input for updating users
input UpdateUserInput {
username: String
}
# input for relay cursor connections
input ConnectionInput {
first: Int
after: String
last: Int
before: String
}
type MessageConnection {
edges: [MessageEdge]
pageInfo: PageInfo!
... id: Int! # unique id for the group
name: String # name of the group
users: [User]! # users in the group
messages(messageConnection: ConnectionInput): MessageConnection # messages sent to the group
}
# a user -- keep type really simple for now...
type Mutation {
# send a message to a group
createMessage(message: CreateMessageInput!): Message
createGroup(group: CreateGroupInput!): Group
deleteGroup(id: Int!): Group
leaveGroup(id: Int!): Group # let user leave group
updateGroup(group: UpdateGroupInput!): Group
login(user: SigninUserInput!): User
signup(user: SigninUserInput!): User
}
type Subscription {

Sweet! Now let’s update our resolvers and business logic to handle input types instead of individual variables. The changes are minimal:

Step 8.4: Add Input Types to Resolvers and Logic

Changed server/data/logic.js

...  to(message) {
return message.getGroup({ attributes: ['id', 'name'] });
},
createMessage(_, createMessageInput, ctx) {
const { text, groupId } = createMessageInput.message;
return getAuthenticatedUser(ctx)
.then(user => user.getGroups({ where: { id: groupId }, attributes: ['id'] })
.then((group) => {
... users(group) {
return group.getUsers({ attributes: ['id', 'username'] });
},
messages(group, { messageConnection = {} }) {
const { first, last, before, after } = messageConnection;
// base query -- get messages from the right group
const where = { groupId: group.id };
... }],
}));
},
createGroup(_, createGroupInput, ctx) {
const { name, userIds } = createGroupInput.group;
return getAuthenticatedUser(ctx)
.then(user => user.getFriends({ where: { id: { $in: userIds } } })
.then((friends) => { // eslint-disable-line arrow-body-style
... });
});
},
updateGroup(_, updateGroupInput, ctx) {
const { id, name } = updateGroupInput.group;
return getAuthenticatedUser(ctx).then((user) => { // eslint-disable-line arrow-body-style
return Group.findOne({
where: { id },
include: [{

Changed server/data/resolvers.js

...    updateGroup(_, args, ctx) {
return groupLogic.updateGroup(_, args, ctx);
},
login(_, signinUserInput, ctx) {
// find user by email
const { email, password } = signinUserInput.user;
return User.findOne({ where: { email } }).then((user) => {
if (user) {
// validate password
... return Promise.reject('email not found');
});
},
signup(_, signinUserInput, ctx) {
const { email, password, username } = signinUserInput.user;
// find user by email
return User.findOne({ where: { email } }).then((existing) => {
if (!existing) {

That’s it!

Input Types on the Client

We need the GraphQL requests on our client to match the input type updates we made on our server.

Let’s start by updating GROUP_FRAGMENT with our new ConnectionInput:

Step 8.5: Add Input Types to Mutations

Changed client/src/graphql/group.fragment.js

...      id
username
}
messages(messageConnection: $messageConnection) {
edges {
cursor
node {

This will super simplify all GraphQL requests that return Group types:

Step 8.5: Add Input Types to Mutations

Changed client/src/graphql/create-group.mutation.js

...import GROUP_FRAGMENT from './group.fragment';const CREATE_GROUP_MUTATION = gql`
mutation createGroup($group: CreateGroupInput!, $messageConnection: ConnectionInput = { first: 1 }) {
createGroup(group: $group) {
... GroupFragment
}
}

Changed client/src/graphql/group-added.subscription.js

...import GROUP_FRAGMENT from './group.fragment';const GROUP_ADDED_SUBSCRIPTION = gql`
subscription onGroupAdded($userId: Int, $messageConnection: ConnectionInput){
groupAdded(userId: $userId){
... GroupFragment
}

Changed client/src/graphql/group.query.js

...import GROUP_FRAGMENT from './group.fragment';const GROUP_QUERY = gql`
query group($groupId: Int!, $messageConnection: ConnectionInput = {first: 0}) {
group(id: $groupId) {
... GroupFragment
}

Our other mutations will also look cleaner with their fancy input types as well:

Step 8.5: Add Input Types to Mutations

Changed client/src/graphql/create-message.mutation.js

...import MESSAGE_FRAGMENT from './message.fragment';const CREATE_MESSAGE_MUTATION = gql`
mutation createMessage($message: CreateMessageInput!) {
createMessage(message: $message) {
... MessageFragment
}
}

Changed client/src/graphql/login.mutation.js

...import gql from 'graphql-tag';const LOGIN_MUTATION = gql`
mutation login($user: SigninUserInput!) {
login(user: $user) {
id
jwt
username

Changed client/src/graphql/signup.mutation.js

...import gql from 'graphql-tag';const SIGNUP_MUTATION = gql`
mutation signup($user: SigninUserInput!) {
signup(user: $user) {
id
jwt
username

Finally, we need to update our React Native components to pass in the right values to the new input types. The changes are pretty trivial:

Step 8.6: Add Input Types to Screens

Changed client/src/screens/finalize-group.screen.js

...
const createGroupMutation = graphql(CREATE_GROUP_MUTATION, {
props: ({ ownProps, mutate }) => ({
createGroup: group =>
mutate({
variables: { group },
update: (store, { data: { createGroup } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({ query: USER_QUERY, variables: { id: ownProps.auth.id } });

Changed client/src/screens/messages.screen.js

...  options: ownProps => ({
variables: {
groupId: ownProps.navigation.state.params.groupId,
messageConnection: {
first: ITEMS_PER_PAGE,
},
},
}),
props: ({ data: { fetchMore, loading, group, refetch, subscribeToMore } }) => ({
... // GROUP_QUERY is used by default)
variables: {
// load more queries starting from the cursor of the last (oldest) message
messageConnection: {
first: ITEMS_PER_PAGE,
after: group.messages.edges[group.messages.edges.length - 1].cursor,
},
},
updateQuery: (previousResult, { fetchMoreResult }) => {
// we will make an extra call to check if no more entries
...
const createMessageMutation = graphql(CREATE_MESSAGE_MUTATION, {
props: ({ ownProps, mutate }) => ({
createMessage: message =>
mutate({
variables: { message },
optimisticResponse: {
__typename: 'Mutation',
createMessage: {
__typename: 'Message',
id: -1, // don't know id yet, but it doesn't matter
text: message.text, // we know what the text will be
createdAt: new Date().toISOString(), // the time is now!
from: {
__typename: 'User',
... },
to: {
__typename: 'Group',
id: message.groupId,
},
},
},
... const groupData = store.readQuery({
query: GROUP_QUERY,
variables: {
groupId: message.groupId,
messageConnection: { first: ITEMS_PER_PAGE },
},
});
... store.writeQuery({
query: GROUP_QUERY,
variables: {
groupId: message.groupId,
messageConnection: { first: ITEMS_PER_PAGE },
},
data: groupData,
});
... }); // check whether the mutation is the latest message and update cache
const updatedGroup = _.find(userData.user.groups, { id: message.groupId });
if (!updatedGroup.messages.edges.length ||
moment(updatedGroup.messages.edges[0].node.createdAt).isBefore(moment(message.createdAt))) {
// update the latest message
updatedGroup.messages.edges[0] = {
__typename: 'MessageEdge',

Changed client/src/screens/signin.screen.js

...
const login = graphql(LOGIN_MUTATION, {
props: ({ mutate }) => ({
login: user =>
mutate({
variables: { user },
}),
}),
});
const signup = graphql(SIGNUP_MUTATION, {
props: ({ mutate }) => ({
signup: user =>
mutate({
variables: { user },
}),
}),
});

Unlike with the previous tutorials in this series, this one doesn’t have a flashy ending. Everything should be working as if nothing ever happenend, but under the hood, we’ve vastly improved the way we make GraphQL requests to gracefully adapt to future changes to our Schema!

Fragments, default variables, and input types are essential tools for designing scalable GraphQL schemas to use in everchanging complex applications. They keep our code lean and adaptable. Apply liberally!

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

You can view the code for this tutorial here

--

--