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.
A request is a method for a client to communicate with a remote server. The server processes the request and sends the appropriate response.
There are 2 main request types(methods) in the HTTP protocol:
GET
- In which the goal of the request is to access something. For example, for accessing this page, the browser sent a GET
request to the host server, which returned the page for the browser to process.
The server can determine the requested resource by examining the URL to which the request was sent.
<aside>
⚠️ Note that the GET
method should not be used for accessing sensitive information if there is a caching mechanism. In these cases, you should use POST
.
</aside>
POST
- In which the goal is to cause a change in state or side effects on the server. For example, creating a user account.
<aside>
ℹ️ POST
parameters are sent in the request body, which is more suitable for sensitive data. Additionally, POST requests are not cached.
</aside>
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:
</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
}
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.
query
using the GET
method - This is recommended for querying non-sensitive data that can be cached. Take a look at the executeGet
function for querying using the GET
method in ScandiPWA.query
using the POST
method - This is recommended for querying sensitive data that should not be cached. Take a look at the fetchQuery
function for querying using the POST
method in ScandiPWA.mutation
using the POST
method - This is recommended for defining GraphQL mutations. Take a look at the fetchMutation
function for mutate using the POST
method in ScandiPWA.<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>
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 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:
executeGet
- To make a request with a GraphQL query
using the GET
method. This function has the advantage of allowing resource caching.fetchQuery
- To make a request with a GraphQL query
using the POST
method. This function does not allow resource caching.fetchMutation
- To make a request with a GraphQL mutation
using the POST
method. This function does not allow resource caching.<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>
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);
}
// ...
executeGet
with .then()
?The executeGet
function accepts 3 parameters:
preparedQueryObject
- queryObject
prepared with the prepareQuery
function
name
- Define a suitable identifier for the request. This will be useful when looking up the request in the Network tab of the developer tools.
If the same name is used for a different request, you may receive unexpected results or invalid data from the API.
cacheTTL
- Stands for Cache Time To Live, which indicates how many seconds you want a response to be cached.
A common practice is to set it to 30 days, but you may need to decrease or increase it. Another option is to set the cacheTTL
to 0, which indicates the response should not be cached.
Examples of use:
src/util
directory.executeGet
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);
}
// ...
fetchQuery
with .then()
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:
fetchQuery
with multiple queries to fetch initial data in a contextmutation
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);
}
// ...
fetchMutation
with .then()
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:
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.
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:
Cache-Control
header equal to some number. It caches the request for the time specified in that header. Most requests coming from Varnish have the Cache-Control
header set to no-cache
, which bypasses the browser cache entirely.GET
request, such as static assets, images, videos, and GraphQL data. Basically, all GET
requests are cached by Varnish.<aside> ➡️ In general, please note that:
</aside>
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:
Cache-Control
header. This may lead to uncontrollable caching on the client's browser. Add new query param to all requests, this should create new cache entries in the browser cache.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
The request functions will return an object with the queries or mutations you defined in the function as keys. For example:
The query document:
src/query/AdyenPaymentStatus.query.js
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:
src/query/AmastyAdd.query.js
src/query/AmastyItems.query.js
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)
]);