Overview
In this section, we will:
- Select fields, including fields of related objects
- Filter results with arguments
- Pass arguments as variables
- Reuse fields with fragments
- Send a query from code, without Sandbox
- See how GraphQL validates every request against the schema
Need a reminder of what’s in the API? See The Course Data.
Objective 1: Select nested fields
Selection sets
The fields inside { } are called a selection set. When a field returns an object instead of a plain value, like a policy’s policyholder, you must give it its own selection set:
{
policies {
policyNumber
policyholder {
name
email
}
}
}
Relationships work in both directions. A policyholder can list their policies:
{
policyholder(id: "ph1") {
name
policies {
policyNumber
type
}
}
}
Using the Documentation panel
You don’t have to memorize field names. Apollo Sandbox reads the API’s schema and lists everything you can ask for in the Documentation panel on the left.
- Start at Root. Selecting Root near the top shows the root types:
Query(fields for reading data) andMutation(fields for changing data). - Click a field to drill in. Clicking
policiesshows its arguments (liketype), what it returns (a list ofPolicy), and below that, every field aPolicyhas, such aspolicyNumber,monthlyPremium, andpolicyholder. - Read the descriptions. Some fields include a short explanation written into the schema. For example,
monthlyPremiumsays it’s in US dollars. - Add fields without typing. Click the ⊕ button next to a field to add it to the query in the editor. Sandbox adds the surrounding braces for you.
A field that returns an object, like policyholder, needs its own selection set. The Documentation panel shows this: its type is Policyholder, not a plain value like String or Float.
Exercise
This time, build the query without typing it, using only the Documentation panel.
✏️ In Apollo Sandbox, clear the editor, then:
- In the Documentation panel, select Root near the top.
- Click the ⊕ button to the right of
Query.
✏️ Keep using the ⊕ buttons to build this query. Sandbox names the operation Query for you, so the button that runs it says Query instead of Run. Click it:
query Query {
policies {
policyNumber
policyholder {
name
email
}
}
}
Hint: When you click ⊕ next to a field that returns an object, like policies, Sandbox adds it and opens it, so you can add the fields inside it. Clicking a field’s name opens it without adding it.
Solution
Click to see the solution
From Query, click ⊕ next to policies. Sandbox opens policies. Click ⊕ next to policyNumber, then next to policyholder. Sandbox opens policyholder. Click ⊕ next to name and email.
Your response should be:
{
"data": {
"policies": [
{ "policyNumber": "AUTO-100001", "policyholder": { "name": "Maria Alvarez", "email": "maria.alvarez@example.com" } },
{ "policyNumber": "HOME-100002", "policyholder": { "name": "Maria Alvarez", "email": "maria.alvarez@example.com" } },
{ "policyNumber": "AUTO-100003", "policyholder": { "name": "James Okafor", "email": "james.okafor@example.com" } },
{ "policyNumber": "LIFE-100004", "policyholder": { "name": "Priya Raman", "email": "priya.raman@example.com" } },
{ "policyNumber": "RENTERS-100005", "policyholder": { "name": "Priya Raman", "email": "priya.raman@example.com" } }
]
}
}
Objective 2: Filter with arguments and variables
Arguments
Fields can accept arguments. The policies field accepts an optional type argument:
{
policies(type: AUTO) {
policyNumber
}
}
AUTO has no quotes because type is an enum: one of a fixed set of values (AUTO, HOME, LIFE, RENTERS) defined by the schema.
Variables
Hard-coding values into a query works for exploring, but applications pass them as variables instead. A variable is declared on the operation with a $ and a type, then used in place of the value:
query PoliciesByType($type: PolicyType) {
policies(type: $type) {
policyNumber
type
}
}
The values are sent separately, as JSON. In Apollo Sandbox, they go in the Variables panel:
{ "type": "AUTO" }
PoliciesByType is the operation name. It’s optional, but it makes requests easier to identify in logs and tools.
Why this query starts with query
Every query you’ve written so far started with a bare {, with no query keyword:
{
policies {
policyNumber
}
}
That’s query shorthand, and the query keyword is optional in exactly that case: a single query with no name and no variables. The two forms below are the same request:
{ policies { policyNumber } }
query { policies { policyNumber } }
You must write query as soon as you want to name the operation or declare variables, because there’s nowhere else to put the name or the $variable list. That’s why the example above is query PoliciesByType($type: PolicyType) and not a bare {.
Most applications write the keyword every time, even for simple queries, so operations are named and easy to find in logs. Mutations have no shorthand. They always start with mutation.
Multiple variables and fields
One operation can declare several variables, separated by commas, and ask for several top-level fields at once. The response contains one entry per field:
query PolicyAndHolder($policyId: ID!, $policyholderId: ID!) {
policy(id: $policyId) {
policyNumber
}
policyholder(id: $policyholderId) {
name
}
}
A variable’s type must match the argument it’s used for. policy(id: ID!) requires an ID, so $policyId is declared as ID!. The ! makes the variable required: the request fails if you leave it out. $type: PolicyType above has no !, so it’s optional.
Exercise
An agent’s dashboard shows a policyholder’s contact details next to a list of policies of one type.
This asks for two separate pieces of information, a policyholder and a list of policies, in a single request.
✏️ Write one query named AgentDashboard that uses two variables:
$policyholderId, to get a policyholder’snameandemail$type, to get thepolicyNumberandtypeof every policy of that type
✏️ Run it for policyholder ph2 and AUTO policies. Your response should be:
{
"data": {
"policyholder": { "name": "James Okafor", "email": "james.okafor@example.com" },
"policies": [
{ "policyNumber": "AUTO-100001", "type": "AUTO" },
{ "policyNumber": "AUTO-100003", "type": "AUTO" }
]
}
}
Hint: Check the Documentation panel for the type each argument expects. That’s the type your variable needs.
Solution
Click to see the solution
Query:
query AgentDashboard($policyholderId: ID!, $type: PolicyType) {
policyholder(id: $policyholderId) {
name
email
}
policies(type: $type) {
policyNumber
type
}
}
Variables:
{ "policyholderId": "ph2", "type": "AUTO" }
$policyholderId is required (ID!) because policyholder(id: ID!) requires an ID. $type is optional because policies(type: PolicyType) is too. Try removing type from the variables: you’ll get every policy.
Objective 3: Reuse fields with fragments
What is a fragment?
The same object often appears in more than one place in a query. When it does, you list the same fields each time. A fragment is a named set of fields that you write once and reuse with ...:
query PolicyholderNames {
first: policyholder(id: "ph1") {
...ContactDetails
}
second: policyholder(id: "ph2") {
...ContactDetails
}
}
fragment ContactDetails on Policyholder {
name
email
}
fragment ContactDetails on Policyholdernames the fragment and says which type its fields come from. A fragment can only be used where that type is returned....ContactDetails(three dots, then the name) is replaced by the fragment’s fields, as if you’d typed them there.first:andsecond:are aliases. A response can’t have two entries namedpolicyholder, so an alias gives each one its own name.
The response is the same as if you’d written out name and email both times. Fragments only change how the query is written. The GraphQL documentation covers fragments with the rest of the query basics.
Why use them
In a frontend app, fragments let each component declare the fields it needs. A PolicyCard component can own a PolicyCard fragment, and every query that shows a policy card includes it. When the card needs a new field, you change the fragment once. Apollo Client’s documentation recommends this pattern, called colocating fragments.
Exercise
The agent’s dashboard now shows a policy card in two places: under the policyholder’s details, for each of their policies, and in the list of policies of one type. Each card shows a policy’s policyNumber, type, and monthlyPremium.
✏️ In Apollo Sandbox, update your AgentDashboard query from Objective 2:
- Add the policyholder’s
policiesunder theirnameandemail. Objective 2’s query doesn’t have this yet. - Use one fragment named
PolicyCardfor the policy fields in both places.
✏️ Run it for policyholder ph2 and AUTO policies. Your response should be:
{
"data": {
"policyholder": {
"name": "James Okafor",
"email": "james.okafor@example.com",
"policies": [
{ "policyNumber": "AUTO-100003", "type": "AUTO", "monthlyPremium": 210.75 }
]
},
"policies": [
{ "policyNumber": "AUTO-100001", "type": "AUTO", "monthlyPremium": 142.5 },
{ "policyNumber": "AUTO-100003", "type": "AUTO", "monthlyPremium": 210.75 }
]
}
}
Solution
Click to see the solution
query AgentDashboard($policyholderId: ID!, $type: PolicyType) {
policyholder(id: $policyholderId) {
name
email
policies {
...PolicyCard
}
}
policies(type: $type) {
...PolicyCard
}
}
fragment PolicyCard on Policy {
policyNumber
type
monthlyPremium
}
Both policyholder.policies and policies return Policy objects, so both can use a fragment on Policy. To show a new field on every card, like effectiveDate, you’d add it to PolicyCard only.
Objective 4: Send a query from code
What Sandbox sends
Apollo Sandbox is a convenient way to explore, but an app sends queries itself. A GraphQL request over HTTP is usually a POST with a JSON body. The GraphQL documentation describes its fields:
query: the operation, as a stringvariables: the variables, as a JSON object (optional)operationName: which operation to run, when the string has more than one (optional)
You can send one from the Codespace’s terminal with curl, a command-line tool that sends HTTP requests:
curl -s http://localhost:4001/ -H 'content-type: application/json' --data '{"query":"query PoliciesByType($type: PolicyType) { policies(type: $type) { policyNumber type } }","variables":{"type":"AUTO"}}'
The response is the same JSON you see in Sandbox:
{"data":{"policies":[{"policyNumber":"AUTO-100001","type":"AUTO"},{"policyNumber":"AUTO-100003","type":"AUTO"}]}}
In a browser app, fetch sends the same request:
const response = await fetch("http://localhost:4001/", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
query: `query PoliciesByType($type: PolicyType) {
policies(type: $type) { policyNumber type }
}`,
variables: { type: "AUTO" },
}),
});
const { data, errors } = await response.json();
Check errors as well as data. A request can reach the server and still fail, and the reason is in errors. You’ll see examples in the next few sections.
Most apps don’t call fetch directly. A GraphQL client, like Apollo Client, sends the same request for you, and also caches the results.
Exercise
✏️ Open a second terminal in the Codespace, so the server keeps running in the first one. In the Terminal panel, click +.
✏️ In the second terminal, use curl to send a query for policyholder ph3’s name and the policyNumber of each of their policies. Pass the policyholder’s id as a variable, not in the query string. Your response should be:
{"data":{"policyholder":{"name":"Priya Raman","policies":[{"policyNumber":"LIFE-100004"},{"policyNumber":"RENTERS-100005"}]}}}
Solution
Click to see the solution
curl -s http://localhost:4001/ -H 'content-type: application/json' --data '{"query":"query Holder($id: ID!) { policyholder(id: $id) { name policies { policyNumber } } }","variables":{"id":"ph3"}}'
The query string declares $id, and variables gives it a value. The single quotes around the whole body stop the terminal from treating $id as one of its own variables.
Objective 5: Understand request validation
Every request is checked against the schema
Before any of your server code runs, GraphQL checks the request against the schema. Asking for a field that doesn’t exist fails immediately:
{
policies {
deductible
}
}
The response (trimmed for readability) is:
{
"errors": [{ "message": "Cannot query field \"deductible\" on type \"Policy\"." }]
}
Fields and arguments are checked separately
Arguments are validated the same way: an argument the schema doesn’t declare is rejected before it reaches your code.
Say underwriting wants a list of high-risk policies. Every policy has a riskTier field, so filtering on it seems reasonable:
{
policies(riskTier: HIGH) {
policyNumber
riskTier
}
}
But the request fails. The response (trimmed for readability) is:
{
"errors": [{ "message": "Unknown argument \"riskTier\" on field \"Query.policies\"." }]
}
riskTier is a field you can ask for, but it isn’t an argument that policies accepts. In the Documentation panel, policies lists only one argument: type. Being able to read a value doesn’t mean you can filter by it; the schema has to declare each one.
Next steps
Next, we’ll look inside the server at the schema and resolvers, and add a riskTier argument ourselves.