A request enables a client to communicate with a server using different methods, depending on the intent of the request. To access data, the GET method is used, while the POST method is generally used to request the server to make changes.

ScandiPWA makes use of the GraphQL query language implementation on Magento, which has the concept of queries to request data and mutations to request that the server change data.

To optimize the request/response process, the server implements a caching system that allows the result of some requests to be stored. Then, the stored content can be used to respond to identical requests that follow. This has some advantages and disadvantages that should be taken into consideration when choosing the type of request to make.

It's essential to understand the different places where the response can be cached, as well as how to bypass the cache and ensure that the request reaches the server.

ScandiPWA has a pattern for making GraphQL requests. This pattern involves creating a document that defines queries and mutations, and then using built-in functions to simplify and secure the process.

These functions allow you to easily access the response as an object with keys that have the same name as the queries and mutations defined in the request sent.

However, it is always possible for a request to fail, and properly catching the error is crucial to ensure everything continues to work as intended. It is a good practice to inform the user about the error and provide instructions on how to proceed. This approach leads to a better user experience, despite the presence of an error.

What is a request?

A request is a method for a client to communicate with a remote server. The server processes the request and sends the appropriate response.

Untitled

There are 2 main request types(methods) in the HTTP protocol:

What is a GraphQL request?

GraphQL is a query language that can be implemented on a server to allow the client to select precisely what it needs in the response (using queries) and also to instruct the server to make changes (using mutations).

<aside> ℹ️ This page presents a basic view of GraphQL, for more details, take a look at the official documentation.

</aside>

<aside> ⚠️ To these queries and mutations actually work, the server must implement them. You can learn more about how to work with resolvers in Magento 2 and implement these functionalities:

How to work with Resolvers?

</aside>

What is a GraphQL query?

In essence, a GraphQL query requests data. The below example shows a query and a possible response:

Query example:

query {
    s_wishlist {
        id
        items_count
        updated_at
    }
}

Response example

"data": {
    "wishlist": {
        "id": "10",
        "items_count": 3,
        "updated_at": "2023-08-07 14:00:20"
    }
}

Note that this defines a query s_wishlist, which indicates that the response should include the fields id, items_count, and updated_at. The response will exactly match this definition.

What is a GraphQL mutation?

GraphQL mutations allow you to request modifications to server-side data.

The following example illustrates a mutation defined by ScandiPWA called s_clearWishlist. This is used to communicate with the back end to clear the wishlist.

mutation {
    s_clearWishlist
}

What type of request to use?

When considering the main request methods and the possibilities of GraphQL, there are 3 scenarios to consider for determining the best type of request to use in ScandiPWA.

Untitled

<aside> ℹ️ Note that there is no option to choose the GraphQL mutation using the GET method. This is because passing data to be saved in the URL is considered a bad practice.

</aside>

How does ScandiPWA make requests?

ScandiPWA makes use of GraphQL queries and mutations to communicate with the Magento server.

Instead of hardcoding the GraphQL query and defining complex requests in the code, which would make it hard to maintain and extend. ScandiPWA has a pattern to allow defining query documents and functions to make the request:

ScandiPWA makes use of query documents to allow you to dynamically create a query and use it in other places.

query {
	fieldName {
		nestedField		
	}
}
import { Field } from 'Util/Query';

const exampleQuery = new Field('fieldName')
		.addFieldList([
		    'nestedField'
    ]);

Using a query document, you can define the left example query like in the right example.

Although it may seem easier to define queries as shown in the first example, using query documents has significant advantages for development. They allow queries or mutations to be more reusable and extensible through the use of plugins.

You can learn about working with query documents:

How to work with queries?

How does ScandiPWA make use of the query document?

ScandiPWA request helper functions are defined in util/Request, which allows you to easily make the request using the query document. There are 3 functions:

<aside> ⚠️ You should consider what type of request to use when using these functions.

</aside>

<aside> ℹ️ It is preferable to use await instead of .then(). Using await makes the code more extensible compared to using .then().

</aside>

<aside> ✅ You can define your requests either in dispatch functions or src/util directory!

</aside>

How to send a query using the GET method?

The executeGet function allows you to send a query in the request using the GET method.

Check the example:

import { prepareQuery } from 'Util/Query';
import { executeGet } from 'Util/Request';
import YouQueryClass from "../../query/YouQueryClass.query";

const CACHE_TTL = 86400; // this is one day in seconds

// ...
try {
    const data = await executeGet(
        prepareQuery(
            YouQueryClass.getSpecificQuery()
        ),
        'requestName',
        CACHE_TTL
    );
    // process data
} catch (error) {
    // handle request error
    console.error(error);
}
// ...

The executeGet function accepts 3 parameters:

Examples of use:

How to send a query using the POST method?

The fetchQuery function allows you to send a query in the request using the POST method. This function does not allow caching, making it the best option for queries that may contain confidential information.

Check the example:

import { fetchQuery } from 'Util/Request';
import YouQueryClass from "../../query/YouQueryClass.query";

// ...
try {
    const data = await fetchQuery(YouQueryClass.getSpecificQuery());
    // process data
} catch (error) {
    // handle request error
    console.error(error);
}
// ...

The fetchQuery function accepts only 1 parameter, which is a raw query as returned by the query document, or a list of queries.

<aside> ℹ️ You should not prepare the query for use in fetchQuery, as this is done internally by fetchQuery.

</aside>

Examples of use:

How to send a mutation using the POST method?

The fetchMutation function allows you to send a mutation in the request, in order to change information in the server. This function does not allow caching.

Example:

import { fetchMutation } from 'Util/Request';
import YouQueryClass from "../../query/YouQueryClass.query";

// ...
try {
    const data = await fetchMutation(PlaceQuestionMutationQuery.getSpecificQuery());
    // process data
} catch (error) {
    // handle request error
    console.error(error);
}
// ...

The fetchMutation function accepts only 1 parameter, which is a query as returned by the query document, or a list of queries.

<aside> ℹ️ You should not prepare the mutation for use in fetchMutation, as this is done internally by fetchMutation.

</aside>

Examples of use:

How does request caching work?

When a server receives a request, it processes it and returns the corresponding response. For instance, if 1000 users on a ScandiPWA site request to list products from the same category using GET requests, the server processes all 1000 requests and returns the same response to each of them.

However, this is a waste of resources since the server is processing identical requests to return the same response multiple times.

One solution to this problem is to use a cache system. This allows a response to be stored for a given type of request. Whenever a request is made and its response is already stored, the stored response is returned. This makes the processing faster and avoids wasting resources.

This is particularly true for requests using the GET method. Since the intention of the request is only to retrieve content, it can be easily matched using only the URL. For this reason, requests using the GET method are cached by its URL.

<aside> 🚨 The caching system may be a problem if a request using the GET method has sensitive data in the response, which is not desired to be cached. In these cases, even though the request's goal is to access data, it’s best to use the POST method instead.

</aside>

Requests using the POST method are not cached, since the intention of this method is to allow the server to make changes in its state. Therefore, requests using the POST method always hit the server.

How is the request cached?

Untitled

As the web server returns assets, cacheable assets are stored in different layers. These cached assets are then returned in subsequent requests for the same resource, without the need for the request to hit the server again.

The following cache mechanism may intercept your request on its way to the server:

<aside> ➡️ In general, please note that:

</aside>

How to bypass the cache?

You can bypass any cache and ensure that the request hits the server by modifying the URL. Adding a new query parameter will cause Varnish, ServiceWorker, and the browser to treat the request as new. This technique is useful when you want to:

To add a query parameter using JavaScript, use the following logic:

const oldUrl = /** old URL*/;
const tmpUrl = new URL(oldUrl);
tmpUrl.searchParams.set('v', '1');
const newUrl = tmpUrl.href; // <- use this as new URL

How to handle the response?

The request functions will return an object with the queries or mutations you defined in the function as keys. For example:

The query document:

This query document defines a query adyenPaymentStatus. Hence, the response will include an object named adyenPaymentStatus.

const {
    adyenPaymentStatus
} = await fetchQuery(AdyenPaymentStatusQuery.getAdyenPaymentStatus(orderNumber, cartId));

Another example using multiple queries:

Note that the getQuery method of the AmastyAdd class returns a field named amAdd, while the getQuery method of the AmastyItemsQuery class returns a field named amItems. Therefore, the objects you need to access in the response are amAdd and amItems.

const { amAdd, amItems } = await fetchQuery([
    AmastyAddQuery.getQuery(),
    AmastyItemsQuery.getQuery(cartId)
]);