A Simple Query
In this section, you are going to implement the first GraphQL object types and fields that provide the functionality of a Hacker News clone for querying a feed of links that were posted by other users.
Extending the Schema Definition
Let's start by implementing a feed
field on the Query
root types which allows retrieving a list of Link
elements. In general, when adding a new feature to the GraphQL Schema, the process will look pretty similar every time:
- Extend the GraphQL schema definition (SDL) with new root fields (on the
Query
,Mutation
orSubscription
object types) and new object types and fields, if needed - Implement corresponding resolver functions for the added fields/object types
This process is also referred to as the schema-driven or schema-first development workflow.
So, let's go ahead and tackle the first step, extending the GraphQL schema definition.
In src/schema.ts
, update the GraphQL schema to look as follows:
const typeDefinitions = /* GraphQL */ `
type Query {
info: String!
feed: [Link!]!
}
type Link {
id: ID!
description: String!
url: String!
}
`
Pretty straightforward, right? You're defining a new Link
object type that represents the links that can be posted to Hacker
News. Each Link
entity has an id
, a description
, and a url
field. You're then adding another field to the root Query
type
that allows you to retrieve a list of Link
elements. This list is guaranteed to never be null
(if there isn't anything, the list must be empty) and never contain any elements that are null
- that's what the two exclamation marks are for.
If you wish to read more about GraphQL and nullability, go ahead to GraphQL official documentation.
Implement Resolver Functions
The next step is to implement the resolver function for the feed
query field. One thing we haven't mentioned yet is
that not only root fields, but virtually all fields on the types in a GraphQL schema have resolver functions. So,
you'll add resolver functions for the id
, description
, and url
fields of the Link
type as well.
In src/schema.ts
, add a new list with dummy data as well and update the resolvers
to look as follows:
// 1
type Link = {
id: string
url: string
description: string
}
// 2
const links: Link[] = [
{
id: 'link-0',
url: 'https://graphql-yoga.com',
description: 'The easiest way of setting up a GraphQL server'
}
]
const resolvers = {
Query: {
info: () => `This is the API of a Hackernews Clone`,
// 3
feed: () => links
},
// 4
Link: {
id: (parent: Link) => parent.id,
description: (parent: Link) => parent.description,
url: (parent: Link) => parent.url
}
}
Let's walk through the numbered comments again:
- The
Link
type defines the TypeScript object structure that we wish to use in our code - The
links
variable is used to store the links at runtime. For now, everything is stored only in-memory rather than being persisted in a database - You're adding a new resolver for the
feed
root field. Notice that a resolver always has to be named exactly after the corresponding field from the schema definition - Finally, you're adding three more resolvers for the fields on the
Link
type from the schema definition. We'll discuss what theparent
argument that's passed into the resolver here is in a bit
Go ahead and test the implementation by running the server and navigating to http://localhost:4000/graphql
in your browser. If you expand the documentation of the GraphiQL, you'll notice that another field on the root Query
object type called feed
is now available:
Try it out by sending the following query operation via GraphiQL:
query {
feed {
id
url
description
}
}
Awesome, the server responds with the data you defined in links
:
{
"data": {
"feed": [
{
"id": "link-0",
"url": "https://graphql-yoga.com",
"description": "The easiest way of setting up a GraphQL server"
}
]
}
}
Feel free to play around with the query by removing any fields from the selection set and observing the responses sent by the server.
The Query Resolution Process
Let's now quickly talk about how a GraphQL server resolves incoming requests. As you already saw, a GraphQL operation contains a selections set of fields that have their source in the type definitions of the GraphQL schema.
Let's consider the following query operation from above again:
query {
feed {
id
url
description
}
}
All four fields specified in the query operations selection set (feed
, id
, url
, and description
) can also be found inside the schema definition. You also learned that every field inside the schema definition is backed by one resolver function whose responsibility it is to return the data for precisely that field.
Can you imagine what the query resolution process looks like now? Effectively, the GraphQL server has to invoke all resolver functions for the fields that are selected in the operation and then package up the response according to the selection sets shape. The resolution thus merely becomes a process of orchestrating the invocation of the resolver functions!
Right now all the resolvers for the Link
type follow a very simple and trivial pattern:
Link: {
id: (parent: Link) => parent.id,
description: (parent: Link) => parent.description,
url: (parent: Link) => parent.url
}
First, it's important to note that every GraphQL resolver function receives four input arguments. As the remaining three are not needed in our scenario right now, we're simply omitting them. Don't worry, you'll get to know them soon.
The first argument, commonly called parent
(or sometimes root
) is the result of the previous resolver execution level. Hang on, what does that mean? 🤔
Well, as you already saw, GraphQL operation selection sets can be nested. Each level of nesting (i.e. nested curly braces) corresponds to one resolver execution level. The above query operation. Therefore, has two of these execution levels.
On the first level, it invokes the Query.feed
resolver and returns the entire data stored in links
. For the second execution level,
the GraphQL server is smart enough to invoke the resolvers of the Link
type (because thanks to the schema, it knows that feed
returns
a list of Link
elements) for each element inside the list that was returned on the previous resolver level. Therefore, in all the three
Link
resolvers, the incoming parent
object is the element inside the links
list.
Note: To learn more about this, check out this article.
The implementation of the Link
resolvers is trivial, you can omit them and
the server will work in the same way as it did before 👌 We just wanted you to
understand what's happening under the hood 🚗.