Join us on Nov 7 for GraphQL Summit Virtual
Join API innovators, GraphQL experts, and AI trailblazers on Nov 7 at GraphQL Summit Virtual. Register for free →
We often need to refine the queries that we send to a GraphQL service so that we can retrieve data for something particular. Rather than asking about all the kinds of Fruit
in a database, we might be after a specific one, or fruits of a specific color. We need some mechanism to reduce the scope of the data that we're asking about.
For this purpose, GraphQL provides us with field arguments, which we can use to pass identifying or filtering data - for example, a particular item's id
or name
, or even a date range - so our results are limited to the objects of our interest.
In a schema, arguments are enclosed in parentheses that come after a field's name. Each argument that we define consists of two parts:
fruit(id: ID!): Fruit!
The example above illustrates these two elements. The argument's name is id
; this is the name that we'll use wherever we're writing queries that want to pass a particular id
value to the fruit
field.
Note: In the case of the
Fruit
type, theid
field serves as its unique identifier. This makes itsid
field a reliable reference to use when we're trying to locate a particularFruit
object, and exclude all others. For this reason, defining a field's argument with the name ofid
is a common convention when the goal is to locate and return data about a singular object.
We've also specified that the id
argument has a non-nullable type of ID
. This is provided to set the expectation of what kind of data GraphQL can expect us to send in the query. If we try to pass in a Int
or a Boolean
value as the id
data type when querying the fruit
field, we'll get an error.
Let's compare this new field with the others we've seen on the Query
type.
type Query {fruits: [Fruit!]!featuredStall: Stall!orders: [Order!]fruit(id: ID!): Fruit!}
In contrast to the fruit
field, the other fields on the Query
type enforce something of an "all-or-nothing" approach. We can ask only for the data that's predefined for us; none of these fields allow us to pass arguments to help determine the results we get back.
To remedy this problem, the id
argument that we see on the fruit
field acts as a kind of empty box: as long as the value that we pass into it fits the ID
data type we've defined, the field can perform some logic to locate the single Fruit
object that we're asking for.
We can see how this value gets passed into the field using the query below.
When we specify the fruit
field from the Query
type, we need to pass in a value for id
within the parentheses. As long as the value we pass fits the ID
data type, the GraphQL service resolving the request can return the data for the particular Fruit
object we've requested.
The names that we define for field arguments are up to us to decide on, but it's best practice to use clear naming conventions. In the example above, we see how the id
argument we provide appears to "sync up" with the id
for the corresponding Fruit
object in an almost magical way. However, this is a common misconception about how arguments passed to a field relate to the object type the field returns. Let's clarify this by walking through an example.
We've discussed the elements that make up fruit
, a new field on the Query
type that receives an argument called id
, and returns a single, non-nullable Fruit
object.
fruit(id: ID!): Fruit!
When we look closer at the Fruit
object type, we'll see that it also contains a field called id.
"""Represents a single kind of fruit available at the market,with details about the vendor that sells it"""type Fruit implements Produce {id: ID!"The name of the kind of fruit"name: String!"The unit cost of each fruit, in coins"price: Int!"How many units of the fruit are currently in stock"quantity: Int!"Whether or not this fruit contains seeds a human should consume"hasEdibleSeeds: Boolean"A list of the three most abundant nutrients contained in this fruit"nutrients: [String]"The region this type of fruit was grown in"region: String!"A list of signs that a fruit is ripe"ripenessIndicators: [String]"A reference to the stall that sells this kind of fruit"vendor: Stall}
Though they share the same name, there is no immediate connection between the id
argument and the id
field on the Fruit
type. This is because before any Fruit
data is returned, the id
argument passed into the fruit
field needs to be interpreted, and acted upon, by the GraphQL service. Put another way, the service needs to take in the id
provided by the user, and use it to find the Fruit
object with the corresponding id
.
This means that the Query
type's fruit
field could have defined a completely different argument name than id
, as shown below:
fruit(uniqueFruityIdentifier: ID!): Fruit!
And as long as the GraphQL service understands how to use the uniqueFruityIdentifier
argument to locate the data it needs, there's no problem. (But from a usability standpoint, it makes more sense to keep field arguments simple!)
See? No magic here - just the GraphQL service doing its thing, taking in input and locating the data we want!
We've seen an example with an argument value hardcoded right into the query. As far as reusability goes, this approach is less than ideal. To overcome this, we can use variables in our query to act as placeholders for the values that we'll pass in later. This lets us keep our queries dynamic and responsive to changing data.
Let's rework the query for a particular fruit using query variables instead.
query GetParticularFruit($id: ID!) {fruit(id: $id) {idnamepricehasEdibleSeeds}}
We've made a special addition to this query: in two places, we'll now find an element called $id
. This is how a query, its variables, and arguments might appear in practice, but let's make it a little bit clearer. (Right now there are too many instances of "id" to make sense of!)
query GetParticularFruit($placeholderVariable: ID!) {fruit(id: $placeholderVariable) {idnamepricehasEdibleSeeds}}
We've swapped all the instances of $id
with the much clearer $placeholderVariable
. The dollar sign ($
) is the primary indicator that we're defining a GraphQL variable and it's required - but the rest of the variable's name is up to us!
The query above, GetParticularFruit
, is now dependent on information that comes from outside of it. We're no longer hard-coding an id
value to pass to the fruit
field, so we need to rely instead on something from the surrounding context of where our query is being defined. This might be the code that runs the frontend of an application, which could determine a particular Fruit
to query for based on where the user clicks.
The parentheses that follow GetParticularFruit
indicate that the operation will accept at least one variable. This is where we first define $placeholderVariable
. Whatever the value passed in, we can refer to it anywhere else that we use it as $placeholderVariable
.
query GetParticularFruit($placeholderVariable)
Ultimately, when we pass in an actual value, this variable helps GraphQL figure out all the places the value needs to be inserted.
But GraphQL relies on types, so we can't expect it to predict what kind of data it will receive when we assign $placeholderVariable
a real value. For this reason, we also need to inform the operation what type of data it will be accepting as the variable's value. We want to be able to pass the query a particular "id", which we know from our schema should be of type non-nullable ID
.
Easy enough - we can tell the operation that it should expect a variable called $placeholderVariable
, and that its data type will be a non-nullable ID
, as shown below.
query GetParticularFruit($placeholderVariable: ID!) {
Now the following line of the query should make much more sense. We read in the $placeholderVariable
value, and pass it as the value of the id
argument that the fruit
field receives.
query GetParticularFruit($placeholderVariable: ID!) {fruit(id: $placeholderVariable) {
We know that the fruit
field accepts an id
argument, because it was defined that way in our schema.
type Query {fruit(id: ID!): Fruit!}
Since the id
argument on the fruit
field looks for a value of type ID
, the actual value held in $placeholderVariable
fits this need perfectly - we declared it to be an ID
as well!
We can try out this query in the Apollo Sandbox below. It's preloaded with references to $placeholderVariable
. But if we try to run it as is, we'll get an error!
{"errors": [{"message": "invalid type for variable: 'placeholderVariable'","extensions": {"name": "placeholderVariable","code": "VALIDATION_INVALID_TYPE_VARIABLE"}}]}
We see this error because we're trying to run a query using a variable without a value yet. Apollo Sandbox comes with a way for us to define the actual value that we should pass into the $placeholderVariable
box.
Just below where the query is written in the Sandbox, expand the Variables panel. Here we can define a JSON object containing any and all variables we're passing into this query. In our case, we need just one. Paste the entire object below (including the curly braces) into the Variables panel.
{"placeholderVariable": "F4"}
Notice that in the JSON object where we define the variable's actual value, we omit the dollar sign (
$
) we included in the query.
Now try running the query again.
And we should see the data for the particular Fruit
we've asked for!
Because we can contain the arguments passed to a query within an object, we're not limited to defining just one argument at a time.
A query that uses both the vendor
and fruit
fields from the Query
type below would involve two arguments - the id
for the particular Fruit
, and the id
for the particular Stall
.
type Query {fruits: [Fruit!]!featuredStall: Stall!orders: [Order!]fruit(id: ID!): Fruit!vendor(id: ID!): Stall!}
A query providing the arguments with hardcoded values might look something like this:
query GetParticularFruitAndVendor {fruit(id: "F4") {idnamepricehasEdibleSeeds}vendor(id: "S3") {idname}}
To pass in dynamic variables, we can update the GetParticularFruitAndVendor
so that it expects two variables, separated by a comma, that we can use for each id
argument.
query GetParticularFruitAndVendor($fruitId: ID!, $vendorId: ID!) {fruit(id: $fruitId) {idnamepricehasEdibleSeeds}vendor(id: $vendorId) {idname}}
Try this out in the Apollo Sandbox below, providing a JSON object in the Variables panel that contains a fruitId
of "F4" and a vendorId
of "S3".
{"fruitId": "F4","vendorId": "S3"}
Individual fields can also accept multiple arguments if, for example, they need additional parameters to refine the results they return.
We can use the Order
and Stall
types below to envision a more complex query we might make.
type Order {id: ID!vendor: Stall!items: [Produce!]!orderOffer: Offertotal: Int!}type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce!]!orders(minTotal: Int, maxTotal: Int): [Order!]}
The Order
type has a field called total
, which contains the total amount of money paid for an order. The Stall
type has an orders
field, which accepts two optional arguments: minTotal
and maxTotal
, both of which expect an Int
type.
When querying for order information for a particular vendor, we can envision this orders
field being used to pass in two different values that can define a range for dollar value. This would allow us to query for an individual Stall
type's orders whose total dollar amount falls within a range of values.
Let's see what this might look like in practice when we build a query that can accept input for a specific vendor and value filters.
query GetParticularVendorAndOrderRange($vendorId: ID!$minTotalCoinAmount: Int$maxTotalCoinAmount: Int) {vendor(id: $vendorId) {idnameorders(minTotal: $minTotalCoinAmount, maxTotal: $maxTotalCoinAmount) {idtotal}}}
Take note that we've given the variables - the values beginning with
$
- names that differ from the argument they're passed to. Often the same name will be given to both the variable and the argument that receives it, but we've kept them distinct to be extra clear about their different roles, and the fact that they don't need to match.
We can see that this query consumes three variables: $vendorId
, $minTotalCoinAmount
, and $maxTotalCoinAmount
. But it utilizes them in different places. The vendor
field receives the $vendorId
variable, while the $minTotalCoinAmount
and $maxTotalCoinAmount
variables are applied to the orders
field on the Stall
type.
Try the query out in the Apollo Sandbox below. See if you can return the orders that fall in the dollar range of 100
and 400
for the vendor with the id
of "S1"
!
{"vendorId": "S1","minTotalCoinAmount": 100,"maxTotalCoinAmount": 400}
When a field accepts more than one argument, it can be useful to encapsulate all of its arguments in a new type. We call this an input type, and it's used to organize multiple arguments and ensure that a field receives all of the input it requires. Input types also make organizing and interpreting schema documents much easier, because we can use them in more than one place without needless repetition.
We can refer to the Stall
type to create a new input type containing the minTotal
and maxTotal
arguments passed to the orders
field.
type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce!]!orders(minTotal: Int, maxTotal: Int): [Order!]}
To define an input type, we specify the input
keyword, followed by the type name.
input TotalRangeInput
As with interfaces and object types, the input
type is an object that contains one or more fields. Inside of curly braces, we can define the arguments it will contain as individual fields that specify their data type.
input TotalRangeInput {minTotal: IntmaxTotal: Int}
Note that it's helpful to include the word
Input
when naminginput
types. This helps discern what's aninput
type from other object types included in the schema.
In our schema, we can swap the orders
field definition on the Stall
type to consume the TotalRangeInput
type instead of individual arguments.
input TotalRangeInput {minTotal: IntmaxTotal: Int}type Stall {id: ID!name: String!stallNumber: String!availableProduce: [Produce!]!orders(totalRange: TotalRangeInput): [Order!]}
Now, the orders
field receives a single argument, which we've called totalRange
, that uses the input TotalRangeInput
as its type.
When we use an input
type instead of individual arguments, we'll also need to change how we pass variables into a query.
query GetParticularVendorAndOrderRange($vendorId: ID!, $orderTotalRangeInputVariable: TotalRangeInput) {vendor(id: $vendorId) {idnameorders(totalRange: $orderTotalRangeInputVariable) {idtotal}}}
Take note that we've given the variable of type
TotalRangeInput
the long and exaggerated name of$orderTotalRangeInputVariable
. This is for extra clarity when comparing the different elements in the example query above. In practice, it's common to give a variable the same name as the argument receiving it, with the required variable dollar sign ($
) preceding it.
Test yourself by running this query in the Apollo Sandbox below, and be sure to include all of the necessary variables!
{"totalRange": {"minTotal": 100,"maxTotal": 400,},"vendorId": "S1"}
Schema fields that accept arguments can also define default values. This allows clients (like us!) to run queries without specifying an argument's value - if we leave it blank, the query will use the default value provided in the schema!
In this example, we have a field on our Query
type called fruitsByAvailableSeasonString
. This allows us to pass in a specific filter called season
. If we don't pass in our own value for season
, the query will use the default value of "summer"
!
type Query {fruitsByAvailableSeasonString(season: String = "summer"): [Fruit]}
Try it out in the Sandbox below! Without modifying anything, we'll see results for fruits that are available in the summer season!
Now pass in a string as the value for the season
argument in the Variables panel.
{"season": "winter"}
About
GraphQL.com is maintained by the Apollo team. Our goal is to give developers and technical leaders the tools they need to understand and adopt GraphQL.
GraphQL.com 2024