Two small features of the query language solve problems you will hit in your first real project: needing the same field twice with different arguments, and leaving out parts of a query without writing a second one. After this lesson you will be able to rename fields in a response, toggle fields with variables, and mark old schema fields as deprecated without breaking existing clients.
A GraphQL response mirrors the shape of the query: every selected field becomes a key with the same name. That creates a conflict the moment you ask for the same field twice:
query {
user(id: "1") { name }
user(id: "2") { name }
}Validation rejects this because both selections would be written to the key user. An alias gives each selection its own key. Write the alias, a colon, then the field:
query {
admin: user(id: "1") { name }
editor: user(id: "2") { name }
}The response contains the alias names instead of the field names:
{
"data": {
"admin": { "name": "Ada Lovelace" },
"editor": { "name": "Grace Hopper" }
}
}Aliases work at any depth. Typical uses are fetching one field with different arguments (small: avatar(size: 64), large: avatar(size: 512)) and renaming fields to what your UI expects (fullName: name). The server resolves user exactly as before; only the response key changes.
A directive is an annotation starting with @ that changes how part of a document or schema is handled. Servers may add custom directives, but the built-in ones below work everywhere.
| Directive | Used on | Purpose |
|---|---|---|
| @include(if: Boolean!) | Fields, fragment spreads, inline fragments | Include the selection only when if is true |
| @skip(if: Boolean!) | Fields, fragment spreads, inline fragments | Omit the selection when if is true |
| @deprecated(reason: String) | Field definitions, enum values, arguments, input fields | Mark schema members for removal |
Both directives take a single if argument of type Boolean. The value almost always comes from a variable, so one query can serve several screens:
query Profile($withPosts: Boolean!, $hideEmail: Boolean = false) {
me {
name
email @skip(if: $hideEmail)
posts(limit: 3) @include(if: $withPosts) {
title
}
}
}With variables { "withPosts": false, "hideEmail": true } the response is { "data": { "me": { "name": "Ada Lovelace" } } }. Skipped fields are absent from the result entirely, not set to null, so client code must treat the key as optional. Attaching a directive to a fragment spread toggles a whole block at once: ...OrderDetails @include(if: $detailed). If both directives appear on one field, it is included only when @include is true and @skip is false.
GraphQL APIs evolve in place instead of being versioned. When a field must be replaced, the old one stays in the schema and is marked with @deprecated so tools can warn developers:
type User {
id: ID!
name: String!
fullName: String! @deprecated(reason: "Use `name` instead.")
role: Role!
}
enum Role {
ADMIN
EDITOR
MODERATOR @deprecated(reason: "Moderators were merged into EDITOR.")
}The reason argument is optional but should name the replacement. GraphiQL, Apollo Sandbox and code generators read this metadata through introspection and show a warning. Since graphql-js 16, arguments and input fields can be deprecated as well. Nothing changes at runtime: deprecated fields keep resolving until usage metrics show you can remove them.
@include with a positive variable name ($withPosts) over @skip with a negative one; double negatives are hard to read.Why does the query `{ user(id: "1") { name } user(id: "2") { name } }` fail validation?
alias: field) renames the response key and lets you request one field several times with different arguments.@include(if:) and @skip(if:) conditionally include selections and are usually driven by Boolean variables.null.@deprecated(reason:) marks schema members for removal without breaking clients; deprecate first, remove later.Next lesson: Fragments and Reusable Query Parts — extract repeated selection sets into named fragments and reuse them across queries.