There are two main operations for GraphQL - queries and mutations. If you look at the main schema.gql
file on the backend, you will see a query and mutation object defined. These are the root level operations a client can make.
type Query {
users: [User!]!
user(id: Int!): User!
}
type Mutation {
createUser(createUserInput: CreateUserInput!): User!
updateUser(updateUserInput: UpdateUserInput!): User!
removeUser(id: Int!): User!
}
type User {
exampleField: String!
exampleNum: Int!
}
input CreateUserInput {
exampleField: String!
}
input UpdateUserInput {
exampleField: String
id: String!
}
Semantically, queries are for requesting data and mutations are for creating modifications. In the example above, we can either query users
or user
. The !
means the type is required, otherwise it is nullable.
In order to respond to these queries and mutations, we use resolvers
. In our case, resolvers are classes that tell GraphQL how to respond to these queries. So a resolver for the users
query must return an array of users.
// @Query = Generate a query
// () => [User] = Return type is [User]
// { name: "users" } = name the query "users"
@Query(() => [User], { name: "users" })
findAll() {
// assume usersService returns an array of Users.
return this.usersService.findAll();
}
Our backend uses Code-First GraphQL. That means you should not modify the schema.gql
file by hand. Instead, use the nest g
command to generate GraphQL resources. Modify them as necessary. Our application will use these classes to dynamically generate the schema file.
To generate a type like User
, we use @ObjectType()
:
@ObjectType()
export class User {
@Field(() => String, { description: "Example field (placeholder)" })
exampleField: string;
@Field(() => Int, { description: "Example field (placeholder)" })
exampleNum: number;
}
For input types, we can similarly use @InputType()
.
Resolvers generally aim to resolve an @ObjectType
.
@Resolver(() => User)
export class UsersResolver implements ResolverInterface<User> {
@Query(() => User, { name: "user" })
async findOne() {
return {
exampleNum: 4,
};
}
@ResolveField()
exampleField(@Root() user: User) {
return user.exampleNum + ": exampleField";
}
@ResolveField()
exampleNum(@Root() user: User) {
return 5;
}
}
@Resolver()
and tell it what ObjectType
it is resolving, in this case User
.ResolverInterface
with generic type of the object to be resolved.@Query
and @Mutation
to define any relevant root-level operations.@ResolveField
to outline how to resolve fields on the object. For example, User
here has exampleField
and exampleNum
, so we (optionally) define field resolvers for each of these.Lets say we made the following query:
query GetUser {
user {
exampleField
exampleNum
}
}