Sending GraphQL requests with Node.js in Pipedream

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:

  1. The GraphQL endpoint to send requests to
  2. 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:

  1. 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.

  1. The variables are passed as part of the client.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:

  1. Setting up the GraphQL Client
  2. Defining the query (mutation in this case)
  3. 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:

  1. Within the GraphQL request definition, we’re naming the mutation createProduct
  2. We’re defining the createProduct's variables, which is a single $product with the type ProductInput
  3. We’re passing the product input to the GraphQL client alongside the query definition
  4. We’re using the client.mutate() method, instead of client.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.