Here's a quick summary of everything we released in Q1 2024.

GraphQL

Queries

Queries are entry points on a GraphQL server that provides read access to your data sources.

What are GraphQL Queries?

Queries are entry points on a GraphQL server that provides read access to your data sources. GraphQL queries are a powerful tool for clients as they can dictate the fields they want and the response structure. With a REST API, you will be hitting an API endpoint and getting some fixed data structure according to an API contract agreed upon earlier, but a GraphQL query is very much flexible and dynamic the client is free to fetch whatever it wants. In this article, we will cover the fundamentals of GraphQL queries.

GraphQL Playground

A GraphQL server provides you with a playground, where you can see the GraphQL schema, all the available types, and the queries that you can perform. A GraphQL playground comes in very handy when you do not have a frontend application ready and want to test out your server, also it gives you IntelliSense and simplifies writing operations. It is a place where you can test out your queries and mutations, and see what data they respond with before integrating your queries with a front-end application. A GraphQL playground can be used by both frontend and backend teams as per their own needs.

Hygraph provides an API playground in the dashboard itself, you can create your schema, and relations and then test out your queries using this API playground. Your query would go to the section on the left-hand side and you would get results on the right-hand side.

Hygraph GraphQL Playground

Basic Query

To better understand GraphQL queries, let us take a look at the basic structure of a very simple query.

For example:

# Query
query {
user (id: 1) {
name
email
}
}
# Response
{
"data": {
"user": {
"name": "John Doe",
"email": "johndoe@hygraph.com"
}
}
}

This is a very simple query let us break it down further:

  • An operation type can be either query, mutation, or subscription. Here the keyword query tells us that this operation type is a GraphQL query
  • The field user is the entry point, it is the root field that informs the server that we want to query the user entity.
  • The id is an argument passed with the query to resolve the appropriate data for the user we want. name and email are the fields that we want the server to get from the data source.

One important thing to note is that the query syntax is not set in stone. The root level query exposed by the server could be named getUserById instead of user, and the format to pass arguments could be (where: { id: 1 }) instead of (id: 1).

For example:

query {
getUserById(where: { id: 1}) {
name
email
}
}

The query above is also equivalent to the query we saw earlier. The exact syntax is dependent on the server-side implementation and it will determine how exactly your GraphQL query will look. Information on the exact syntax will be available in the schema documentation in the API playground and you can also use IntelliSense in any GraphQL playground to see what options are available at each step.

Nested Queries

In real-world scenarios, relations between entities often exist and you need to extract data from more than one database table. We can say that a user can have many addresses probably in different countries. So in this case there is a one-to-many relationship between the user and address entities and a one-to-one relationship between the address and country entity.

In order to see all the addresses that belong to a user and the country details for each address, we will need to construct a client-side GraphQL nested query that would get all the user details, details of all his addresses, and for each address the corresponding country details.

Here is how a nested query for the same will look like

# Query
query {
user(id: 1) {
name
email
address {
street
city
country {
name
capital
}
}
}
}
# Response
{
"data": {
"user": {
"name": "John Doe",
"email": "johndoe@hygraph.com",
"address": [{
"street": "123 Main St",
"city": "Berlin",
"country": {
"name": "Germany",
"capital": "Berlin"
}
},
{
"street": "456 Second Ave",
"city": "Mumbai",
"country": {
"name": "India",
"capital": "New Delhi"
}
}]
}
}
}

Querying with Filters

GraphQL queries can be made more powerful and efficient by using filters. Filters are used to retrieve specific data based on a set of criteria, you can consider filters as a where clause in a SQL query. The filters exposed to the client for querying will depend on the GraphQL server implementation. You can use filters on queries and even nested queries to get the exact records that you want to retrieve. For example, If you want to retrieve users whose email has hygraph.com in it and also want to get all contacts for each user that start with the letter A, here is what the query will look like:

# Query
query getUserContacts {
users(where: {email_contains: "hygraph.com"}) {
email
contacts(where: { name_starts_with: "A"}) {
name
}
}
}
# Response
{
"data": {
"users": [
{
"email": "johndoe@hygraph.com",
"contacts": [
{
"name": "Alice",
"phone": "789654123"
},
{
"name": "Andrew",
"phone": "123654878"
}
]
},
{
"email": "janedoe@hygraph.com",
"contacts": [
{
"name": "Ashley",
"phone": "8522123659"
}
]
}
]
}
}

Best Practices For GraphQL Queries

Named Queries & Variables

So far we have only written raw queries that take the raw arguments directly and have no names. In production applications, there can be hundreds of queries and it might get very difficult to manage them if you are not using named queries. Also, you do not have raw argument values like 1 or John Doe, you have variables that store this value and you need to pass these variables to your queries.

Here is our previous example of a query for getting a user:

query {
user (id: 1) {
name
email
}
}

This is an equivalent named query version for the same example:

query getUserById($userId: ID!) {
user(id: $userId) {
name
email
}
}

We saw the operation type earlier (query, mutation, subscription). Here we are also adding an Operation Name - getUserById. The operation name is an explicit and meaningful name given to a GraphQL operation. Although not mandatory, it is recommended to use them as they can greatly aid in debugging and server-side logging. When there is an error either in the network logs or the GraphQL server logs, it is easier to identify the problematic query by name rather than decoding its contents. If you are from a Javascript background, it is similar to giving a name to an anonymous function - it can be worked with without a name, but naming it makes it easier to debug and track down in your codebase. Similarly, GraphQL operation names can help identify different GraphQL requests on the server side, making it a useful tool for debugging.

Instead of hardcoding the arguments to GraphQL queries, we should be using variables. If you notice the code just after the operation name that is - ($userId: ID!). This signifies that the operation accepts a variable named userId which is of the type ID!, this variable is then passed on to the actual root field query i.e. user where it is supplied as value for the argument id. We will learn more about GraphQL variables in this article.

As a best practice, all queries coming from clients should be named queries and should use variables instead of hardcoded arguments in a production application.

Using Fragments

Suppose you're building a social media app that allows users to create and share posts, as well as comment on and like other users' posts. You want to display a list of posts in the user's feed, with information about the post's author, content, and any comments or likes associated with the post.

To retrieve this information, you might use a GraphQL query that looks something like this:

query {
feed {
id
author {
name
profilePictureUrl
}
content
comments {
author {
name
profilePictureUrl
}
text
}
likes {
author {
name
profilePictureUrl
}
}
}
}

This query retrieves the id, author, content, comments, and likes fields for each post in the user's feed. However, notice that the author field is repeated multiple times - once for each of the author fields nested under comments and likes. This can result in a lot of duplicated code and make the query harder to read and maintain.

To simplify the query and make it more modular, you might use a fragment to define the author field once, and then reference it multiple times throughout the query. Here's what that might look like

query {
feed {
id
author {
...authorFields
}
content
comments {
author {
...authorFields
}
text
}
likes {
author {
...authorFields
}
}
}
}
fragment authorFields on User {
name
profilePictureUrl
}

In this updated query, we've defined a fragment called authorFields that includes the name and profilePictureUrl fields for a user. We then reference this fragment multiple times throughout the query, both in the author field at the top level of the query and in the author fields nested under comments and likes.

Using fragments in this way allows us to define common pieces of a query once and reuse them throughout the query. This can make the query more modular, easier to read, and easier to maintain. Additionally, if we ever need to update the fields we're retrieving for a user, we can simply update the author fields fragment, rather than having to update every instance of the author field throughout the query.

Using Aliases

At times it is possible that you might be querying the same field twice in a query with different conditions. For instance, you want to get addresses for a user and you also want to bifurcate the primary and secondary address in the query itself, so in this use case you will write a query as shown below

query getUserAddressDetails($userId: ID! ) {
user(id: $userId) {
address(type: "primary") {
...addressFields
}
address(type: "secondary") {
...addressFields
}
}
}

However, this query would not work as there are two fields with the same name - address, the server would simply give out a validation error for your query. In such cases you need to use aliases in your query this is how the correct query would look like

# Query
query getUserAddressDetails($userId: ID! ) {
user(id: $userId) {
primaryAddress: address(type: "primary") {
...addressFields
}
secondaryAddress: address(type: "secondary") {
...addressFields
}
}
}
# Response
{
"data": {
"user": {
"name": "John Doe",
"primaryAddress": {
"street": "123 Main St",
"city": "Berlin"
},
"secondaryAddress": {
"street": "456 Second Ave",
"city": "Mumbai"
}
}
}
}

Conclusion

In this article, we explored how to GraphQL queries and how powerful they are for clients as they support declarative data fetching. We saw the anatomy of a GraphQL query, checked out basic queries, and explored further depth with nested queries and filtering capabilities. In addition to this, we explored best practices and available tools like the GraphQL playground, fragments, aliases, named queries, and variables, that can help developers optimize their GraphQL queries and build more efficient applications.