Queries in GraphQL are based on two things: an entry point and traversals. The entry point is the initial object (or objects) that are coming from a Query or a Mutation type. This provides a "foothold" in the objects graph.
Then the query can define what other fields and objects it needs in the response. This makes GraphQL queries powerful: the client can define what it needs and it gets exactly that.
For example, a client might want to show the social graph for a user, so it fetches the friends:
query {
user(id: "user1") {
name
friends {
name
}
}
}
A different client might want to show an admin panel and show all the users with their email addresses and permissions:
query {
allUsers {
name
email
permissions {
name
}
}
}
Both queries above use the same object graph, but they get different data. This gives GraphQL queries a lot of flexibility and if used well provides a faster user experience.
It's all too easy to query everything and then select what is needed on the client-side. But this does not utilize the flexibility of the queries and goes against the best practices for GraphQL.
But queries are a lot more powerful than just selecting what fields the client needs. GraphQL provides a lot of features to fine-tune their behavior. In this chapter we'll look into the most useful ones and how to use them.
What gives the most versatility for GraphQL queries is the ability to select the fields they need in the response. A type might have many fields, but if the client needs just a few of them, then it consumes less resources to omit the unneeded ones.
For example, a User
object has a username
, a bio
, and an email
address:
type User {
username: String!
email: String
bio: String
}
type Query {
user(username: String!): User
}
If the client does not need the email
address, it can choose not to ask for that:
# query
query MyQuery {
user(username: "user1") {
username
bio
}
}
And the result:
{
"data": {
"user": {
"username": "user1",
"bio": "Lorem ipsum"
}
}
}
While most fields directly map to columns in a database, it is not always the case and the client should not assume it is "free" on the backend. For example, the User
object in the database might only contain the username
but not the email
address. In a common scenario, returning the email means sending a call to Cognito (or a similar user directory) and extract the value from the response.
Because of this, clients should query only for data that they will actually use.
Fields can also be types defined in the schema. Getting a field that is not scalar is how a query can move from object to object in the graph. For example, showing the friends of a user means asking for the friend object and then their fields.
# schema
type User {
username: String!
email: String
bio: String
friends: [User!]!
}
type Query {
user(username: String!): User
}
# query
query MyQuery {
user(username: "user1") {
username
friends {
username
}
}
}
And the result:
{
"data": {
"user": {
"username": "user1",
"friends": [
{
"username": "user2"
},
{
"username": "user3"
}
]
}
}
}
As we've seen in the Arguments chapter, fields of any type can define what arguments they need. These can be "top-level", such as a Query or a Mutation, but every field for every type can have arguments too.
A client query must provide all required arguments and it can fill any optional ones. When a query does not pass any arguments for a field the parentheses are missing.
For example, a query in the schema might define an optional argument and the client can choose whether to define that or not:
# schema
type Query {
# No ! after String, so this is an optional argument
allUsers(search: String): [User]
}
Both of these queries are valid:
query Query1 {
allUsers {
username
email
}
}
query Query2 {
allUsers(search: "admin") {
username
email
}
}
Arguments for fields work the same. Here, the username
is required, while the after
is optional:
# schema
type Ticket {
text: String!
# POSIX time
created: Int!
}
type User {
username: String!
# the argument is optional
tickets(after: Int): [Ticket]
}
type Query {
# the argument is required
user(username: String!): User
}
Defining arguments for a nested field works the same as for the query itself:
query Query1 {
user(username: "user1") {
username
tickets(after: 1639907490) {
text
created
}
}
}
query Query2 {
user(username: "user1") {
username
# no argument here
tickets {
text
created
}
}
}
As queries are text-based and arguments are usually user-supplied, for example a search field on a website, it's a bad idea to use string concatenation. Instead, GraphQL provides a way to separate the fixed part of the query from the variable part.
In this query, the variable part is the text
argument of the search
query:
query SearchQuery {
search(text: "users") {
text
location
}
}
With string concatenation it's all too easy to end up with something like this:
// don't do this
// search is the user-provided value
const graphQLQuery = `query SearchQuery {
search(text: "${search}") {
text
location
}
}`;
The problem here is that the value can contain characters that "break away" from the query argument. For example, if a user searches for something"
, then the resulting query is going to be:
query SearchQuery {
search(text: "something"") {
text
location
}
}
With the double "
, the query is now invalid and the user will get an error.
To declare a variable for a query, give it a name and add it to the query object. Then whenever you want to define that value, use the variable name:
query SearchQuery($text: String) {
search(text: $text) {
text
location
}
}
Then pass the variable separately, which is dependent on the client library you are using.
For example, AppSync variables are in variables
field as a JSON object:
Unlike SQL injection, string concatenation in a GraphQL query is not a security vulnerability. This is because it happens on the client-side, so a malicious actor can already send any query they wish.
Except when the dynamic part is defined by a client different than the one sending the request, such as a Lambda function that gets parameters from a HTTP request. In that case, not using variables can open security vulnerabilities.