Introduction
In the previous article, we talked about the basic set-up for GraphQL projects as well as the use of GraphiQL for executing queries and mutations. With this knowledge, we can now dive deeper into the world of GraphQL input types. GraphQL is a query language, which can be used with different languages like Javascript, C#, Scala, and more. However, in this article we will focus on Facebook's Javascript implementation of GraphQL called graphql-js. We will also introduce some code snippets and examples illustrating how different types can be written in the GraphQL schema language. This is not language-specific, however, and the main focus will be on designing a GraphQL schema using the graphql-js classes. We will concentrate on the often misunderstood topic of using GraphQL input object types in mutations.
Input and output types
According to GraphQL specification, when we deal with its type system, we have to discuss two different categories of types:
- output type can be used for definition of data, which is obtained after query execution;
- input types are used as a query parameters, e.g., payload for creating a user. In graphql-js library we basically have two different types, which can be used as objects. GraphQLObjectType (an output type), and GraphQLInputObjectType (an input type).
Designing our mutation
Now let's consider creating a schema for saving users into a database. We will not use a real database, as it is not the main focus of our article. The in memory database is good enough for us to get started. For more information, please check out this repository on my GitHub account, where the in-memory database is implemented. You can quickly start with this command
git clone git@github.com:atherosai/graphql-gateway-apollo-express.git
The types in the schema have some additional fields than in the following examples, but it still illustrates all the concepts precisely. We can start building a schema by defining the data structure. In GraphQL this means defining our GraphQL types, which we can do using the GraphQLObjectType from the graphql-js library. By defining the GraphQLObjectType and also a corresponding query or mutation, we can then query for desired fields to retrieve from the database. When we query a field in GraphQL, we are basically asking for a unit of data. Each field can be a scalar type or an enum type. A field is also sometimes called a leaf, a name from graph theory related to tree graphs.
To create a new instance of GraphQLObjectType in graphql-js we have to use at least some of these parameters:
- name* - Each name of an object type has to be unique across the schema;
- fields* - Fields can be an object with field definitions or a function, which returns an object with field definitions. Each field must have a type definition, and the other optional attributes are description and default values. An object has to have at least one field;
- description - This is an optional attribute, but is really useful for GraphQL schema documentation.
Now let's try to create a simple User Object with the following fields: id, username, email, phone, role, firstName, lastName and two timestamps createdAt and updatedAt.
import {
GraphQLString,
GraphQLID,
GraphQLObjectType,
GraphQLNonNull,
} from 'graphql';
const User = new GraphQLObjectType({
name: 'User',
description: 'User type definition',
fields: () => ({
id: {
type: new GraphQLNonNull(GraphQLID),
},
username: {
type: new GraphQLNonNull(GraphQLString),
},
email: {
type: GraphQLString,
},
phone: {
type: GraphQLString,
},
firstName: {
type: GraphQLString,
},
lastName: {
type: GraphQLString,
},
}),
});
export default User;
It can be also written in SDL
"""User type definition"""
type User {
id: ID!
username: String!
email: String
phone: String
firstName: String
lastName: String
}
Both ways of defining our type offer their own advantages and disadvantages. However, If you want to use the Schema Definition Language for more complex schema, it is better to use some third party tool like graphql-tools. More information can be found, for example, in Apollo docs or in some of other articles. Now let’s consider designing a mutation for adding users. If you do not use Relay, the query string for executing this mutation may look like this:
mutation {
createUser(email: "david@atheros.ai", firstName: "David", lastName: "Mráz", phone: "123456789", username: "a7v8x") {
user {
id
username
firstName
lastName
email
phone
}
}
}
The parameters passed into a createUser() are called arguments. All the fields we are asking for is then called selection set. An argument, for example, could be a scalar argument like GraphQLString or also GraphQLInputObjectType from the graphql-js library. The mutation above can be written in our schema in the following way:
import {
GraphQLList,
GraphQLNonNull
} from 'graphql';
import { isEmail } from 'validator';
import { createUser } from '../../operations/users-operations';
import CreateUserPayload from "./CreateUserPayload";
const UserMutations = {
createUser: {
type: CreateUserPayload,
args: {
username: {
type: new GraphQLNonNull(GraphQLString),
},
email: {
type: GraphQLString,
},
phone: {
type: GraphQLString,
},
firstName: {
type: GraphQLString,
},
lastName: {
type: GraphQLString,
},
},
resolve: async ({}, { input }) => {
if (input.email && !isEmail(input.email)) {
throw new Error('Email is not in valid format');
}
return createUser(input);
},
},
}
export default UserMutations;
We can see that we do not want to pass the id, as the server generates an id for every new user. In resolver, we have added a simple email validation function for new user emails using a library called validator js. The email validation can be also done by defining the custom scalar type. For simplicity’s sake, we’ll leave that to another article. As for the mutation arguments, if you do not use some static type checking like Flow, this can lead to different errors, as a lot of arguments have to be specified. For these reasons, it is not considered a good practice. This problem can be solved with the so-called parameter object pattern. The solution is to replace a lot of arguments with an input object and then we can only reference the input object and access its properties with dot notation. This pattern is enforced in Relay by default. It is commonly considered a best practice to use an object, called input, as an argument for the specific mutation. I would recommend not only using it in Relay, but also in Apollo or just any other schema that does not us a GraphQL client. The need for the use of this pattern increases with the number of arguments. However, it is good to follow this practice in every mutation.
Applying parameter object pattern on our mutation
Now let’s apply parameter object pattern on our createUser mutation. First, we need to define the UserInput, which can be used as a parameter object for the mutation. This code accomplishes this goal
import {
GraphQLString,
GraphQLInputObjectType,
GraphQLNonNull,
} from 'graphql';
const CreateUserInputType = new GraphQLInputObjectType({
name: 'CreateUserInput',
description: 'Input payload for creating user',
fields: () => ({
username: {
type: new GraphQLNonNull(GraphQLString),
},
email: {
type: GraphQLString,
},
phone: {
type: GraphQLString,
},
firstName: {
type: GraphQLString,
},
lastName: {
type: GraphQLString,
},
}),
});
export default CreateUserInputType;
or again in SDL:
input CreateUserInput {
username: String!
email: String
phone: String
firstName: String
lastName: String
}
You may ask yourself, why do I need to have two different types of input and output? Isn’t it possible to just use GraphQLObjectType on both arguments and field definitions? The reason is that GraphQL needs two different structures. One is for taking input values and coercing them into server-side values, and the other is responsible for fetching data from a server. If we have these two types together, the type has to implement both of these structures. This problem is also discussed in GraphQL specification
The Object type defined above is inappropriate for re‐use here because Objects can contain fields that express circular references or references to interfaces and unions, neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system.
Another difference is also, that GraphQLNonNull, basically responsible for not allowing null values in the query, has a different meaning. When it comes to GraphQLObjectType, if we query for the field in the object, the return value from the resolver function has to do two things. It needs to contain the field with the correct attribute, and it cannot be equal to null. As for input types, we need to specify the field, wrapped by a GraphQLNonNull instance, even just to execute the mutation. You can also check out this thread. Nevertheless, it is possible to avoid the boilerplate, which occurs if you define the fields twice. In my projects, I often assign the fields, used in both GraphQLObjectType and GraphQLInputObjectType, to a specific object. These fields are then imported into each type using the object spread operator. However, you have to be able to avoid circular dependencies and other issues, which come up when designing a more complex schema using graphql-js. Now we have defined the GraphQLInputObjectType, so it is possible to replace our previous mutation with the following code
import {
GraphQLList,
GraphQLNonNull
} from 'graphql';
import { isEmail } from 'validator';
import { createUser } from '../../operations/users-operations';
import CreateUserInput from "./CreateUserInputType";
import CreateUserPayload from "./CreateUserPayload";
const UserMutations = {
createUser: {
type: CreateUserPayload,
args: {
input: {
type: new GraphQLNonNull(CreateUserInput),
},
},
resolve: async ({}, { input }) => {
if (input.email && !isEmail(input.email)) {
throw new Error('Email is not in valid format');
}
return createUser(input);
},
},
}
export default UserMutations;
We can observe some reduction in complexity. This does not have such a high impact if we just use GraphiQL for executing the mutation:
mutation createUser {
createUser(input: {
username: "test",
email: "test@test.cz",
phone: "479332973",
firstName: "David",
lastName: "Test"
}) {
user {
id
username
email
phone
firstName
lastName
}
}
}
However, in a real app we often use variables instead. When we pass the mutation variable input using some frontend GraphQL caching client like Apollo, Relay, or even with some promise based HTTP client like Axios, we can then benefit from reducing costly string building. We pass variables separately from the query document and also reduce the code significantly. If we do not have an input object, the query with variables looks like this:
mutation createUser($email: String, $firstName: String, $lastName: String, $phone: String, $username: String!) {
createUser(email: $email, firstName: $firstName, lastName: $lastName, phone: $phone, username: $username) {
user {
id
firstName
lastName
phone
email
username
}
}
}
Nevertheless, by rewriting the mutation with the parameter object pattern we can then write it in the following format and significantly simplify our code:
mutation createUser($input: UserInput!) {
createUser(input: $input) {
user {
id
firstName
lastName
phone
email
username
}
}
}
There is a big advantage of designing a mutation like this. We can reduce the complexity of the frontend code and follow best practices in our project. The importance of input types increases with the number of arguments we have in a mutation. However, we should use best practices even if the mutation payload has just one argument.
Did you like this post? You can clone the repository with examples and project setup.
Feel free to send any questions about the topic to david@atheros.ai.
Top comments (3)
Thank you for the post, David.
Your signature formatting needs markdownified btw :)
Oh right, thanks for letting me know :) I have updated that, looks ok to me now!
Yupz, looks good~ You're welcome and thanks for the update~