Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 59

Building Webhook Handlers in Express.js

Recommended resources

Hello and welcome back!

In our last lesson, you successfully registered your first webhook subscription. You configured your app to listen for the orders/create topic and saw the raw payload appear in your console. This confirmed that the communication channel between Shopify and your app is open.

Now, the natural next step is to make that communication useful. A simple console.log proves the connection, but the real power comes from processing the data within that payload. Today's lesson focuses on exactly that. We will build upon your Express.js knowledge to create a webhook handler endpoint that processes incoming webhook payloads. We'll move from just receiving data to understanding it, extracting it, and structuring our code to act on it.

The Anatomy of a Webhook Handler

From your full-stack experience, you know that a backend endpoint doesn't just receive data; it needs to be prepared for the specific structure of that data. A Shopify webhook is a POST request sent to your registered URL, and it has a very specific anatomy.

  1. HTTP Headers: These contain crucial metadata about the request. The most important for us right now are:

    • X-Shopify-Topic: Tells you which event triggered the webhook (e.g., orders/create).
    • X-Shopify-Shop-Domain: Tells you which store the event originated from.
    • X-Shopify-Hmac-Sha256: A signature that we will use in the next lesson to verify the request is authentic.
  2. Request Body: This contains the main data payload as a JSON string. For an orders/create event, this will be a complete Order object.

Your task as a developer is to build a handler that can reliably parse this request, act on the data, and respond correctly.


From Raw Request to Usable Data in Express.js

In a standard Express.js app, you might use the express.json() middleware to automatically parse incoming JSON bodies into req.body. However, with Shopify webhooks, there's a small but critical twist. To verify the webhook's authenticity (our next topic), we will need access to the raw, unparsed request body.

The express.json() middleware consumes the request stream and makes the raw body unavailable. The solution is to use a different middleware configuration for our webhook endpoint.

Let's examine a robust, standalone example of an Express.js webhook handler. This will clarify the core components required before we integrate them into our Shopify CLI project structure.

Building a Shopify Webhook Handler (Node.js + Express)

The article 'Building a Shopify Webhook Handler (Node.js + Express)' from Tunnelwise provides an excellent, self-contained code example for a production-safe handler. It perfectly illustrates the key pieces we need to understand.

Please review the code snippet under the heading '2. Set up your Express server'. As you read, focus on these four key points: The use of express.raw({ type: 'application/json' }). This middleware is crucial for making the raw body available for verification later. How headers like X-Shopify-Topic are retrieved using req.get(). The logic for handling a specific topic: if (topic === 'orders/create') { ... }. How a successful response is sent using res.status(200).send('OK'). (For now, you can skim over the verifyHmac function itself; we will dive deep into that in our next lesson.)

As you saw in the example, the handler performs several distinct steps:

  1. It configures Express to read the raw body.
  2. It captures the topic and shop domain from the headers.
  3. It parses the raw body (Buffer) into a JSON object using JSON.parse(body.toString('utf8')).
  4. It uses conditional logic (like an if or switch statement) based on the topic to decide how to process the payload.
  5. It immediately sends a 200 OK response to acknowledge receipt.

Integrating Processing Logic into Your Shopify App

Fortunately, the Shopify CLI and the @shopify/shopify-api library handle some of this boilerplate for you. In our last lesson, we set up a handler callback. This callback function is where our processing logic will live. The library passes the topic, shop, and body (as a string) directly to our function.

Let's upgrade our previous handler from a simple logger to something more intelligent.

Our previous handler looked like this:

// In web/index.js
callback: async (topic, shop, body, webhookId) => {
  const payload = JSON.parse(body);
  console.log("Payload:", payload);
}

Now, let's process it. Imagine our app needs to track order totals. We can extract just the information we need.

// In web/index.js
callback: async (topic, shop, body, webhookId) => {
  const payload = JSON.parse(body);

  // Extract specific, useful information from the payload
  const orderId = payload.id;
  const totalPrice = payload.total_price;
  const currency = payload.currency;
  const customerEmail = payload.customer ? payload.customer.email : 'No customer info';

  console.log(
    `✅ Received order ${orderId} from ${shop}. Total: ${totalPrice} ${currency}. Customer: ${customerEmail}`
  );

  // Here you could save this info to your app's database,
  // call another API, send an email, etc.
}

This is much more powerful. You are no longer just acknowledging data; you are interpreting it.


The Importance of a Speedy Response

One of the most critical rules of handling webhooks is to respond with a 200 OK status code as quickly as possible. Shopify expects this confirmation within 5 seconds.

If your handler takes too long to respond (e.g., it's processing a large file or calling a slow third-party API), Shopify will assume the delivery failed. It will then retry sending the same webhook again, potentially multiple times. This can lead to your app processing the same order multiple times, which could have serious consequences (like sending multiple confirmation emails or updating inventory incorrectly).

Best Practice: The webhook handler itself should only do two things:

  1. Verify the webhook's authenticity (next lesson!).
  2. Acknowledge the request with a 200 OK.

Any time-consuming processing should be handed off to a background job queue (e.g., using a library like BullMQ or a service like AWS SQS). The handler adds a job to the queue and immediately responds, while a separate worker process picks up the job and performs the heavy lifting.

Test your understanding!

You've created a webhook handler for orders/create. Inside the handler, you've written code that generates a custom PDF invoice for the order and emails it to the customer. You notice in your logs that some customers are receiving duplicate invoices. What is the most likely cause of this issue, based on what we just discussed?

Show answer

The most likely cause is that the process of generating the PDF and sending the email is taking longer than Shopify's 5-second timeout. Because your server isn't responding with a 200 OK in time, Shopify assumes the delivery failed and retries, triggering your logic a second time and sending a duplicate email. The solution would be to offload the PDF generation and email sending to a background job.


Hands-On: Building Out Your Handler

Let's apply these concepts. We are going to enhance our orders/create handler and add a new one for products/update.

1. Update shopify.app.toml
First, tell Shopify we also want to listen for product updates.

# In shopify.app.toml

[webhooks]
api_version = "2024-04" # Or your target version

[[webhooks.subscriptions]]
topics = [ "orders/create" ]
uri = "/api/webhooks"

# Add this new subscription
[[webhooks.subscriptions]]
topics = [ "products/update" ]
uri = "/api/webhooks"

2. Enhance Your Handlers in web/index.js
Now, let's add the processing logic for both topics.

// In web/index.js
import { WebhookTopic, DeliveryMethod } from "@shopify/shopify-api";

// ... inside the shopifyApp() configuration object
  webhooks: {
    path: "/api/webhooks",

    // 1. Update the OrdersCreate handler
    [WebhookTopic.OrdersCreate]: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/api/webhooks",
      callback: async (topic, shop, body, webhookId) => {
        console.log(`--- Handling a ${topic} webhook for ${shop} ---`);
        const payload = JSON.parse(body);
        
        // Process the order payload
        console.log(
          `Processing Order ID: ${payload.id}, Total: ${payload.total_price} ${payload.currency}`
        );
      },
    },

    // 2. Add the new ProductsUpdate handler
    [WebhookTopic.ProductsUpdate]: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/api/webhooks",
      callback: async (topic, shop, body, webhookId) => {
        console.log(`--- Handling a ${topic} webhook for ${shop} ---`);
        const payload = JSON.parse(body);

        // Process the product payload
        console.log(
          `Processing Product ID: ${payload.id}, Title: '${payload.title}'`
        );
      },
    },

    // ... keep the mandatory GDPR webhooks
  },
// ...

Important: Make sure you have WebhookTopic and DeliveryMethod imported from @shopify/shopify-api.

3. Test Your Handlers
Restart your dev server (npm run dev). Now, use the Shopify CLI to trigger test events for both topics.

  • Trigger an orders/create event:

    npm run shopify webhook trigger -t orders/create
    

    (Follow the prompts for delivery address). Check your server console for the processed order message.

  • Trigger a products/update event:

    npm run shopify webhook trigger -t products/update
    

    Check your server console for the processed product message.

You have now built a webhook handler that can differentiate between topics and extract specific, meaningful data from each payload.


Conclusion

Today, we transformed our simple webhook listener into a true data processing endpoint. You've learned how to parse incoming webhook payloads, extract relevant information, and structure your code to handle different events.

Key Takeaways:

  • A webhook handler's primary job is to parse the POST request from Shopify, containing headers for metadata and a body for the data payload.
  • Intelligent processing involves extracting specific fields from the JSON payload to perform business logic.
  • Your handler must send a 200 OK response within 5 seconds. Long-running tasks should be delegated to a background process to avoid timeouts and duplicate events.
  • You can handle multiple topics by adding multiple subscriptions in your shopify.app.toml and corresponding handlers in your web/index.js configuration.

Next Steps

Our handler is now functional, but it's not yet secure. The endpoint we've created is public on the internet. How do we ensure that the requests it receives are genuinely from Shopify and not from a malicious actor trying to send fake data?

In our next lesson, we will address this critical security concern. You will learn how to verify the authenticity of incoming webhooks using their HMAC signature, a non-negotiable step for any production-level Shopify app.