見出し画像

ShopifyのWebhookイベントで実際に処理をする方法

今回の記事は個人的なメモです。
Shopifyでは様々なタイミングでWebhookイベントをトリガーすることができますが、注文時に特定の処理を行いたいという要件がありましたので、詳細をメモしておきます。

今回はNext.jsを使用してフロントエンドを構築していることを前提に話を進めます。
以下が要件です。

● 注文が完了した時点で、ShopifyのWebhookをトリガーにしてAPIエンドポイントを呼び出します。
● API内で、受け取ったWebhookリクエストが正当なものかを判定します。
● リクエストが正当であれば、Webhookによって送信されたデータを使用して処理を行います。

以上が一般的なフローとなりますね。
それでは、早速Webhookの作成に取りかかりましょう。


1.Webhookを作成する

Shopifyの管理画面にアクセスし、「通知」を選択します。

「通知」メニュー内の「Webhookを作成」を選択し、新しいWebhookを作成します。

Webhookが作成されたら、特定のイベントが発生した際にWebhookをトリガーするように設定します。
具体的なコールバックAPIを設定します。
これにより、Shopifyの特定のイベントが発生した時にAPIが呼び出される準備が整いました。

Webhookが作成されると、SharedKeyという認証用のキーが生成されますので、これを環境変数として設定しておきましょう。

2.APIを作成する

今回はNext.jsを使用してフロントエンドを作成しているという前提で、APIを作成するためにNext.jsのAPI Routesを活用します。

APIを実装する前に、ShopifyのWebhookに関する重要なポイントを確認しておきましょう。

ShopifyのWebhookでは、1回以上のリクエストが送信される可能性があることに注意してください。

Webhookに関する一般的な事実として、同一のWebhookIdからのイベントが複数回発行されることがあるということです。

3.冪等性について

Webhookのように複数回の処理が発生する場合や、APIで複数のデータが紐づく場合、必ず冪等性(idempotency)を確保する必要があります。

数学において、冪等性(べきとうせい、英: idempotence、「巾等性」とも書くが読み方は同じ)は、大雑把に言って、ある操作を1回行っても複数回行っても結果が同じであることをいう概念である。まれに等冪(とうべき)とも。抽象代数学、特に射影(projector)や閉包(closure)演算子に見られる特徴である。"idempotence" という単語はラテン語の "idem"(同じ=same)と"potere"(冪=power)から来ている。

https://ja.wikipedia.org/wiki/%E5%86%AA%E7%AD%89

近年の疎結合なアーキテクチャでは、APIを介して各システムが疎く連携するため、トランザクションの範囲外でデータベースにアクセスすることが必要となります。

また、非同期な処理が主流であり、強い整合性が要求される処理には原則として使用しない方が良いでしょう。
例えば、決済時に在庫数を自社のデータベースと連動させる場合、必ず在庫が1つだけ減るような仕組みが必要です。
このような場合、冪等性を確保しないと、既に売り切れている商品への注文や、販売数のみが増えて在庫数のずれが生じる可能性があります。

冪等性を確保するためには、同じ処理は必ず同じ結果になるようにする必要があります。
また、エラーが発生した場合にはどこでエラーが発生し、データの不整合が生じているかを検知できる仕組みが必要です。
Webhookのリクエストをキューに格納し、エラーリトライの試行回数を超えた場合にはデッドレターキューに入れてエラーを通知し、原因を特定できる仕組みが必要となる場合もあります。

また、同じWebhookIdのリクエストが複数回処理されないように、RedisやDynamoDBなどを使用してWebhookIdの存在を確認し、存在しない場合にのみ後続の処理を実行する判定が必要となります。

4.実際に処理を書いていく

それでは、まずは必要なライブラリをインストールしましょう。

$ yarn add -D crypto raw-body ioredis

Webhookの複数回の発行を防ぐために、Redis(Upstash)を活用します。

ここではioredisを使用するので、ioredisを初期化しておく必要があります。
以下のコードを使用してioredisを初期化しましょう。

// redisClient.ts

import Redis from 'ioredis';

export const redisClient = new Redis(`rediss://:${process.env.REDIS_PASSWORD ?? ''}@${process.env.REDIS_HOST ?? ''}:${process.env.REDIS_PORT ?? ''}`, {
  tls: { rejectUnauthorizedfalse }
});
// api/v1/order.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import getRawBody from 'raw-body';
import crypto from 'crypto';
import { RedisKey } from 'ioredis';
import { redisClient } from 'redisClient';

//-----------------------------------------------------------
// api
//-----------------------------------------------------------
const Index = async (req: NextApiRequest, res: NextApiResponse) => {
  const rowBody = await getRawBody(req);
  const hmacHeader = req.headers['x-shopify-hmac-sha256'];
  const digest = crypto
    .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SHARED_KEY || '')
    .update(rowBody)
    .digest('base64');

  // リクエストがPOSTかどうかをチェック
    if(req.method != 'POST'throw new Error('不正なリクエストです');

  // リクエストが正しいか検証
  if (digest != hmacHeader) throw new Error('不正なリクエストです');

  try {
    // データをパース
    const body = JSON.parse(rowBody.toString());

    // sessionにwebhookIdがあるかをチェックする
    const webhookId = await redisClient.get(`webhook_id_${body.id}`);

    // webhookIdが無ければ
    if (!webhookId) {

      // webhookIdをsessionに保存する
      await redisClient.set(`webhook_id_${body.id}`, body.id, 'EX'60 * 60 * 24 * 6);

      // ここで何かしたい処理をする
    }
  } catch (e) {
    console.log(e);
    return;
  }

  res.send('finished');
};

export const config = {
  api: {
    bodyParser: false
  }
};

export default Index;

やっていることはシンプルで、以下の2点です。

● headers に含まれる x-shopify-hmac-sha256 を検証し、リクエストが正当かどうかを判断します。
● WebhookId を Redis に保存し、既に存在する場合は処理をスキップします。存在しない場合にのみ処理を実行します。

以上が今回の内容でした。
Webhookの利用においては冪等性を確保する仕組みやエラーハンドリングの重要性を理解し、適切に活用することが求められます。
これからもWebhookを積極的に活用して、システムの機能拡張や効率化を図っていきましょう。

それでは。

この記事が気に入ったらサポートをしてみませんか?