Sending GraphQL requests with Node.js in Pipedream
Pipedream makes it easy to connect to APIs, including GraphQL-based APIs, within your workflows using Python or Node.js.
Once your API keys are connected to your Pipedream workspace, you’ll be able to use them with no-code actions and triggers and leverage them within Node.js or Python code.
In this short guide, we’ll show you how to use the Shopify Admin GraphQL API to perform queries and mutations in Node.js code steps with axios
a general HTTP client or with urql
a specialized GraphQL Client.
GraphQL vs REST APIs
If you're familiar with how GraphQL APIs differ from REST APIs, skip ahead to learn about performing queries and mutations.
REST APIs typically organize endpoints around resources. For example, if you’re interacting with a product on Shopify REST API, you have the CRUD (Create, Read Update and Delete) endpoints:
# Create a new product
POST /product
# Get a specific product
GET /product/{product_id}
# Update a specific product
PUT /product/{product_id}
# Delete a product
DELETE /product/{product_id}
This combination of URL structure and HTTP method for specific actions gives a de-facto standard for integrating with APIs.
However, RESTful APIs have some downsides. If you need to retrieve data across different related resources, you’ll be forced to make multiple API calls, and the roundtrip times will add latency as well as count towards your API rate limits. Also, you may only need specific fields from a specific object, and not the entire object.
This fine grain control is where GraphQL shines. Instead of multiple endpoints categorized by resource, GraphQL exposes a single HTTP endpoint. For example, Shopify’s Admin GraphQL API uses a single endpoint for all queries for a specific merchant:
https://my-store.myshopify.com/admin/api/2024-04/graphql.json
One endpoint and method to rule them all
GraphQL only exposes a single endpoint because the payload of your request contains the instructions to retrieve (a.k.a. query) or update (a.k.a. mutate) data.
This collapses the logic of resources to retrieve into a single query. For example, if you needed to retrieve an order but also retrieve specific customer details from that order, in a RESTful API you might need to perform two API requests:
// Get the order details
const order = await axios.get(`https://mystore.com/api/orders/1234`)
// Then retrieve the customer details
const customer = await axios.get(`https://mystore.com/api/customers/${order.customer.id}`)
Whereas in GraphQL, you have the flexibility to retrieve nested or related resources in a single query:
order(id: "gid://shopify/Order/1234") {
total
shippingAddress
billingAddress
customer {
firstName
lastName
email
phone
}
}
That syntax may look funny, but the resulting response body will include both the order details and customer details specified in a single API request. This gives you more fine grain control over your queries and increases performance if your operations rely on related resources.
Also, the same method POST
is used for both queries and mutations. Even if you’re only retrieving data, you’ll still use the POST
method. This is because a GET
method doesn’t contain an HTTP body, which is required for a GraphQL server to understand exactly what data you’re querying for.
So really, the major difference comes down to crafting the payload of the request.
Authenticating GraphQL requests
Thankfully, REST APIs and GraphQL APIs aren’t so different in this regard.
Many REST APIs use the headers of an HTTP request to accept an API key, or token. And this is also the norm when it comes to GraphQL requests.
For example, the both the Shopify Admin REST and GraphQL APIs use the X-Shopify-Access-Token
header in the HTTP request for authentication.
Performing GraphQL Queries with axios
axios
can be used to perform REST API requests, but it can also be used to perform GraphQL requests. This is because both RESTful APIs and GraphQL APIs use the HTTP protocol.
The other benefit of using axios
in Pipedream code steps is the tight integration with Pipedream’s dashboard through the @pipedream/platform
.
For example, the Shopify Developer App uses the Pipedream fork of axios
to perform the default Node.js query:
import { axios } from "@pipedream/platform"
export default defineComponent({
props: {
// Connect your Shopify account to this step
shopify_developer_app: {
type: "app",
app: "shopify_developer_app",
}
},
async run({steps, $}) {
// define the query
const data = {
"query": `{
shop {
id
name
email
}
}`,
}
// use axios to perform a GraphQL query
return await axios($, {
method: "post",
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
headers: {
// pass your Shopify API access token to authenticate the request
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
data,
})
},
})
As you can see in the example above, we define the payload of the request’s query and then use that payload in the POST request to the GraphQL endpoint.
The result returns our data:
{
"data": {
"shop":{
"id":"gid://shopify/Shop/55352656077",
"name":"pipedream-dev",
"email":"pierce@pipedream.com"
}
}
}
Passing variables to axios
GraphQL queries
You may need to provide specific IDs or arguments to search resources in GraphQL queries. You can use plain string interpolation with axios
to perform these requests.
For example, if we wanted to retrieve a specific order by it’s ID, you can use regular JavaScript string interpolation to pass the ID inside of the query:
const orderId = "gid://shopify/Order/5880746049741";
// define the query, and pass the orderId within the payload
const data = {
"query": `{
order(id: "${orderId}") {
id
createdAt
// any other details you'd like to include
}
}`,
}
// use axios to perform a GraphQL query
return await axios($, {
method: "post",
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
headers: {
// pass your Shopify API access token to authenticate the request
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
data,
})
You may notice the double quotes around the orderId
within the payload. The downside of using “naked” requests like this is you’ll have to make sure to massage the output into the correct format.
The double quotes wouldn’t be necessary for integer arguments for example, but it is required for string arguments.
Performing GraphQL mutations with axios
Using a similar method as above, you can pass dynamic variables to a GraphQL mutation payload:
import { axios } from "@pipedream/platform"
export default defineComponent({
props: {
shopify_developer_app: {
type: "app",
app: "shopify_developer_app",
}
},
async run({steps, $}) {
// these variables could be defined in upstream steps
// just an example of how to pass variables inside of a mutation via axios
const title = "Sweet new product";
const productTitle = "Snowboard";
const vendor = "JadedPixel";
// define the mutation, notice that the "query" key is unchanged.
//. the main difference is adding the "mutation" keyword in the value
const data = {
"query": `mutation {
productCreate(input: {title: "${title}", productType: "${productType}", vendor: "${vendor}"}) {
product {
id
}
}
}`,
}
// Send the GraphQL request
return await axios($, {
method: "post",
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
headers: {
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
data,
})
},
})
However notice one key difference, the presence of the mutation
leading keyword in the query
value of our payload.
This instructs the GraphQL server that this is a mutation a.k.a. an update action on the server.
The input
object defines the input of arguments, and the properties inside of the object define what data should be returned in the HTTP response. In our example, we return the generated unique product ID.
That covers the three basic methods of using axios
to perform GraphQL requests. It’s a convenient and simple approach, but you may run into issues with formatting variables in your GraphQL requests.
Next we’ll learn how to use a dedicated GraphQL client to perform requests and inject variables instead, which takes an extra set up step but might be worth the extra effort for you.
Performing GraphQL Queries with urql
Here’s the full example, sending a simple Shopify Admin GraphQL API request using urql
with your Shopify account connected to Pipedream:
import { gql, Client, fetchExchange } from '@urql/core';
export default defineComponent({
props: {
shopify_developer_app: {
type: "app",
app: "shopify_developer_app",
}
},
async run({steps, $}) {
// create the GraphQL Client
// 1. define the endpoint
// 2. pass your API credentials
const client = new Client({
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
exchanges: [fetchExchange],
fetchOptions: () => {
return {
headers: {
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
};
},
});
// define your GraphQL query
const query = `
query {
shop {
id
name
email
}
}
`
// send the GraphQL request
return await client.query(query);
},
})
Let’s breakdown the 3 major steps to this code step.
Creating the client
The first step to performing any GraphQL request is setting up a GraphQL Client. The GraphQL Client needs to be configured with at least two mandatory pieces:
- The GraphQL endpoint to send requests to
- The authentication headers for the request
In urql
, we can pass the endpoint to the url
parameter. And the fetchOptions
parameter allows use to modify the underlying fetch
request to include the X-Shopify-Access-Token
header for authentication:
const client = new Client({
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
exchanges: [fetchExchange],
fetchOptions: () => {
return {
headers: {
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
};
},
});
Defining the payload
The second step is designing the payload of the GraphQL request. This is defining what data we’re like to retrieve.
Here we can use a simple string:
// define your GraphQL query
const query = `
query {
shop {
id
name
email
}
}
`
Next in this tutorial, we’ll show you how to send dynamic data.
Sending the request
After setting up the client and defining the payload, you’re now ready to send the GraphQL request:
// send the GraphQL request
return await client.query(query);
And that’s it. We’ll follow these three steps for queries and mutations alike.
Sending GraphQL requests with dynamic variables
We’ll keep the same GraphQL Client creation steps as described before, but this time we’ll modify the query to include a dynamic argument:
// define your GraphQL query
const query = `
query getOrder($id: ID!) {
order(id: $id) {
id
createdAt
}
}
`
// define the variables
const variables = { id: "gid://shopify/Order/5880746049741" }
// send the GraphQL request
return await client.query(query, variables);
A few differences from this approach from our plain old axios
approach:
- The GraphQL query is wrapped with a
query getOrder($id: ID!) {
wrapping statement.
The getOrder
keyword defines the name of the GraphQL query, and the $id: ID!
portion defines what kinds of arguments are accepted.
$id: ID!
translates to, this query accepts an $id
variable that is an ID
type and is required.
- The
variables
are passed as part of theclient.query
function.
The variables are shared with the query as part of the GraphQL Client’s .query()
function. So that way you have a cleaner interface into sharing variables to the request, and don’t have to worry about formatting strings. The GraphQL Client will handle the formatting of the data for you.
Performing GraphQL Mutations
Now that you’re got a handle on how to send GraphQL queries with dynamic variables, there’s not much difference to sending GraphQL mutations.
It’s the same 3 steps we covered before:
- Setting up the GraphQL Client
- Defining the query (mutation in this case)
- Sending the request with the variables
Here’s the same product creation mutation translated from the axios
example we did earlier:
import { gql, Client, fetchExchange } from '@urql/core';
export default defineComponent({
props: {
shopify_developer_app: {
type: "app",
app: "shopify_developer_app",
}
},
async run({steps, $}) {
// create the GraphQL Client
// 1. define the endpoint
// 2. pass your API credentials
const client = new Client({
url: `https://${this.shopify_developer_app.$auth.shop_id}.myshopify.com/admin/api/2024-04/graphql.json`,
exchanges: [fetchExchange],
fetchOptions: () => {
return {
headers: {
"X-Shopify-Access-Token": `${this.shopify_developer_app.$auth.access_token}`,
"Content-Type": `application/json`,
},
};
},
});
// define your GraphQL query
const query = `
mutation createProduct($product: ProductInput!) {
productCreate(input: $product) {
product {
id
}
}
}
`
// send the GraphQL request
const variables = {
product: {
title: "Sweet new product",
productType: "Snowboard",
vendor: "JadedPixel"
}
}
return await client.mutation(query, variables);
},
})
The major differences are:
- Within the GraphQL request definition, we’re naming the mutation
createProduct
- We’re defining the
createProduct
's variables, which is a single$product
with the typeProductInput
- We’re passing the product input to the GraphQL client alongside the query definition
- We’re using the
client.mutate()
method, instead ofclient.query()
But otherwise it’s the same structure as sending a GraphQL query. And now you’ve mastered the different combinations of sending queries or mutations with GraphQL APIs.
HTTP clients vs GraphQL clients
GraphQL is still using the HTTP protocol to receive requests. So you can still use your favorite Node.js or Python HTTP clients to interact with GraphQL APIs.
However, setting up a GraphQL Client in your Node.js or Python code steps in Pipedream may be beneficial for sending more complex queries or mutations that use dynamic variables due to potential issues with request data formatting.
Once of the benefits of Pipedream is the flexibility to choose when fits your use case.