Nuxt3のserver/apiとuseFetchを使用してShopifyへサーバーAPIリクエストを実装
  • 2022.03.15に公開
  • Nuxt3
  • 1. Shopify APIとの連携
  • No.3 / 5
現在このカテゴリーは、Nuxt3パブリックベータ版の情報を公開しています。変更の可能性が大きくあるので、最新の情報は Nuxt3の公式サイト をご確認ください。

今回達成すること

クライアントからShopifyへAPIリクエストした場合、DeveloperToolにはアクセストークンが漏洩します。

2022-03-14 20-47-34

今回はこの漏洩対応として、商品取得のリクエストAPIをサーバーから送信する設定を行います。

Shopify APIの前提条件

本記事を進めるにあたっての前提条件は以下の通りです。

1. nuxt-shopifyのインストール

この記事を経由していない方はnuxt-shopifyモジュールをインストールし、型を登録を行なってください。

% yarn add nuxt-shopify
tsconfig.json
{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "types": [
      "nuxt-shopify"
    ]
  }
}

2. nuxt-shopifyセットアップの削除

nuxt.config.tsに記入したnuxt-shopifyのセットアップは使用しません。

全て削除してください。

nuxt.config.ts
import { defineNuxtConfig } from 'nuxt3'

export default defineNuxtConfig({
  // 以下、削除

  // modules: [
  //   'nuxt-shopify'
  // ],

  // Doc: https://nuxt-shopify-docs.vercel.app/
  // shopify: {
  //   // shopify domain
  //   domain: process.env.SHOPIFY_DOMAIN,

  //   /*
  //     ストアフロントアクセストークン
  //   */
  //   storefrontAccessToken: process.env.SHOPIFY_ACCESS_TOKEN,

  //   /*
  //     unoptimized: true => SDKの拡張
  //     StorefrontAPIで使用できるすべてのフィールドがSDKを介して公開されるわけではありません。
  //     最適化されていないバージョンのSDKを使用すると、独自のクエリを簡単に作成できます。
  //     これを行うには、UMDUnoptimizedビルドを使用します。
  //     Doc: https://shopify.github.io/js-buy-sdk/
  //   */
  //   unoptimized: false,

  //   // 翻訳されたコンテンツを返すための言語を設定する
  //   language: 'ja-JP',
  // }
})

3. アクセストークンとショップドメインを.envに登録する

この記事を参考に、Shopifyの会員登録と商品登録を行なってください。

その後アクセストークンの取得と.envファイルへの登録を行なってください。

4. NetlifyCLIのインストール

この記事ではデプロイまでを実行します。

その際にNetlifyCLIを使用するので、この記事を参考にインストールを行なってください。

Nuxt3のserver/apiを使用する

サーバーサイドからAPIリクエストを行うには、「server/api」ディレクトリ内のtsファイルに処理を記述します。

API Routes

Nuxt は「~/server/api」 ディレクトリにある任意のファイルを自動的に読み込んで、APIエンドポイントを作成します。

各ファイルは、APIリクエストを処理するデフォルト関数をエクスポートする必要があります。

この関数は、プロミスまたはJSONデータを直接返すことができます(またはres.end()を使用します)。

引用: Nuxt 3 - Data Fetching

Shopify APIに必要なファイル群を作成する

「server/api」ディレクトリの作成と、以下ファイル群を作成します。

% mkdir -p server/api/shopify && touch $_/{client.ts,get-product.ts,get-products.ts}
  • 「shopify」ディレクトリ

    • client.ts ... Shopifyセットアップファイル

    • get-product.ts ... 商品コンテンツ取得API

    • get-product.ts ... 商品一覧取得API

shopify-buyのセットアップ

client.tsにShopifyにアクセスするためのセットアップを行います。

shopify-buyを直接呼び出し、ドメイン、アクセストークン、コンテンツの言語を設定します。

server/api/shopify/client.ts
import Client from 'shopify-buy'

const client = Client.buildClient({
  domain: process.env.SHOPIFY_DOMAIN,
  storefrontAccessToken: process.env.SHOPIFY_ACCESS_TOKEN,
  language: 'ja-JP'
})

export default client

shopify-buyの型を追加する

shopify-buyの型が更新されていないのか、Shopifyプロダクトの一部プロパティが登録されていません。

そこでshopify-buyの型を拡張し、今回使用する型を追加します。

まず、Shopifyの型定義ファイルを作成しましょう。

% mkdir types && touch $_/shopify.ts

shopify.tsに3つの型を追加します。

  • image.whdth
  • image.height
  • descriptionHtml
types/shopify.ts
// type: node_modules/@types/shopify-buy/index.d.ts
import type { Product, Image } from 'shopify-buy'

// デフォルトの型に無いプロパティを追加
interface CustomImage extends Image {
  width: number
  height: number
}

export interface CustomProduct extends Product {
  descriptionHtml: string
  images: Array<CustomImage>
}

Shopifyの型の実態は「node_modules/@types/shopify-buy/index.d.ts」で確認できます。

タイプエラーが出た場合はプロパティが定義されているが確認してください。

商品一覧を取得するAPIを作成する

get-products.tsに商品一覧を取得するAPIを作成します。

server/api/shopify/get-products.ts
import type { IncomingMessage, ServerResponse } from 'http'
import { CustomProduct }  from '~/types/shopify'

import client from './client'

/**
 * req は Node.js の http リクエストオブジェクト
 * res は Node.js の http レスポンスオブジェクト
 * next は、次のミドルウェアを起動するために呼び出す関数
 * https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
 */
export default async (_req: IncomingMessage, _res: ServerResponse, _next: any) => {
  console.log('/api/shopify/get-projects')

  let products: CustomProduct[] = []

  await client.product.fetchAll()
    .then((response: CustomProduct[]) => (products = response))

  return products
}
  • client.product.fetchAll() ... アクティブな商品一覧を取得するshopify-buyのメソッド。

商品コンテンツを取得するAPIを作成する

get-product.tsに商品コンテンツを取得するAPIを作成します。

server/api/shopify/get-product.ts
import type { IncomingMessage, ServerResponse } from 'http'
import { CustomProduct }  from '~/types/shopify'

import client from './client'
import Url from 'url'

export default async (req: IncomingMessage, _res: ServerResponse, _next: any) => {
  console.log('/api/shopify/get-project')

  const currentUrl = Url.parse(req.url, true)
  const productId = currentUrl.query.id
  let product: CustomProduct | undefined

  // productIdが存在 && 型stringの場合
  if (productId && typeof productId === 'string') {
    await client.product.fetch(productId)
    .then((response: CustomProduct) => (product = response))
  }

  return product
}
  • Url.parse(req.url, true) ... 第一引数のURLをオブジェクトに変換する。URLパラメーターを取得する目的がある。
  • client.product.fetch(id) ... 引数のidからShopifyの商品を取得するshopify-buyのメソッド。

useFetchで商品一覧APIを呼び出す

「server/api」に作成したAPIはuseFetch('APIファイルパス')で呼び出すことができます。

このAPIリクエストはサーバーサイドでしか実行されません。

商品一覧ページをまるっと書き換えましょう。

pages/products/index.vue
<template>
  <NuxtLayout>
    <ul
      v-for="(product, i) in products"
      :key="`product-${i}`"
    >
      <li>
        <NuxtLink
          :to="{ name: 'products-id', params: { id: product.id } }"
        >
          {{ product.title }}
          {{ product.variants[0].price }}円
        </NuxtLink>

        <ul>
          <li>
            ID: {{ product.id }}
          </li>
          <li
            v-for="(image, imageI) in product.images"
            :key="`image-${imageI}`"
            :style="{ display: 'inline-block', marginRight: '.5rem' }"
          >
            <img 
              :src="image.src" 
              :alt="`${product.title}-${imageI}`"
              :width="image.width / 20"
              :height="image.height / 20"
            />
          </li>

          <li
            v-html="(product.descriptionHtml)"
          />
        </ul>
      </li>
    </ul>
  </NuxtLayout>
</template>

<script setup lang="ts">
  /**
   * server request only
   * const { data: products } = await useAsyncData('get-products', () => {
   *  return $fetch('/api/shopify/get-products')
   * })
   * OR
   * useFetch()
   * 
   * client request
   * const nuxtApp = useNuxtApp()
   * const products = await nuxtApp.$shopify.product.fetchAll()
   */
  const { data: products } = await useFetch('/api/shopify/get-products')
</script>
  • const { data: products } ... useFetchで取得したデータはdataプロパティに格納されている。このdataプロパティに変数名を定義するには{ data: products }とする。これでAPIで取得したデータをproductsで扱うことができる。

useAsyncDataとuseFetchの違い

useAsyncDatauseFetchの違いは下記記事がとても綺麗にまとまっているので、ここでの説明は省略します。

useFetch()useAsyncData() のふたつの違いが気になりますが useFetch() はデータ取得に特化した useAsyncData() のラッパーでしかありません。

両者に本質的な違いはなく useAsyncData(key, () => $fetch(path), options) のように useAsyncData() の第2引数が () => $fetch() になる場合はすべて useFetch() で構いません。

データ取得以外のケースで非同期に処理をするケースというのは限定されることから useFetch() を使用するということで足りることが多いでしょう。

引用: Nuxt 3 の useFetch() と useAsyncData() の使い方 - Zenn

商品コンテンツAPIの呼び出し

商品コンテンツを表示するページからはuseFetch('/api/shopify/get-product')を呼び出します。

pages/products/[id].vue
<template>
  <NuxtLayout>
    <NuxtLink
      to="/products"
    >
      products
    </NuxtLink>

    <template v-if="product">
      <ul>
        <li>
          {{ product.title }}
          {{ product.variants[0].price }}円

          <ul>
            <li>
              ID: {{ product.id }}
            </li>
            <li
              v-for="(image, imageI) in product.images"
              :key="`image-${imageI}`"
              :style="{ display: 'inline-block', marginRight: '.5rem' }"
            >
              <img 
                :src="image.src" 
                :alt="`${product.title}-${imageI}`"
                :width="image.width / 20"
                :height="image.height / 20"
              />
            </li>

            <li
              v-html="(product.descriptionHtml)"
            />
          </ul>
        </li>
      </ul>
    </template>

    <template v-else>
      <p>商品は見つかりませんでした。</p>
    </template>
  </NuxtLayout>
</template>

<script setup lang="ts">
  const route = useRoute()
  /**
   * $fetch options params
   * Doc: https://github.com/unjs/ohmyfetch#%EF%B8%8F-adding-params
  */
  const { data: product } = await useFetch(
    '/api/shopify/get-product',
    { params: route.params }
  )
</script>
  • route.params ... ここには商品のIDが格納されている。

以上で、商品一覧と商品コンテンツをサーバーから呼び出すことができました。

useFetchのオプション

上のコードのように、第二引数の{ params }に任意のパラメーターを渡すことができます。

useFetchには、そのほかにもオプションが用意されています。

  • method: リクエストメソッドを指定。
const { users } = await $fetch('/api/users', { method: 'POST', body: { some: 'json' } })
  • params: パラメーターを渡す。
await $fetch('/movie?lang=en', { params: { id: 123 } })
  • headers: リクエストヘッダの追加。
await $fetch('/movies', {
  headers: {
    Accept: 'application/json',
    'Cache-Control': 'no-cache'
  }
})
  • baseURL: APIパスの前のURLを指定。
await $fetch('/config', { baseURL })

参考: unjs/ohmyfetch - GitHub

Netlifyにデプロイする

今回のShopify APIは、サーバーで稼働するためNetlifyサーバーに環境変数を登録する必要があります。

NetlifyCLI コマンドのenv:setを使用してアクセストークンとドメインを追加します。

% yarn netlify env:set SHOPIFY_DOMAIN <ショップドメイン>.myshopify.com
% yarn netlify env:set SHOPIFY_ACCESS_TOKEN <アクセストークン>

# 登録の確認
% yarn netlify env:list
.---------------------------------------------------------.
|                  Environment variables                  |
|---------------------------------------------------------|
|         Key          |              Value               |
|----------------------|----------------------------------|
| SHOPIFY_DOMAIN       | ....myshopify.com                |
| SHOPIFY_ACCESS_TOKEN | a................                |
'---------------------------------------------------------'

間違って登録した場合は、env:setで上書き、もしくはenv:unsetで削除できます。

% netlify env:unset SHOPIFY_DOMAIN

デプロイコマンドを実行しましょう。

% yarn netlify:deploy

デプロイコマンドを作成してない方は、下記scriptコマンドを追加してください。

package.json
{
  "scripts": {
    "netlify:deploy": "netlify deploy --build --prod --open"
  },
}

無事デプロイできました。

2022-03-15 00-29-05

まとめ

今回はShopifyへサーバーからAPIリクエストを送るよう変更を行いました。

表示コンテンツは変わらないものの、サーバーサイドでの実行なのでアクセストークンの漏洩を防ぐことができます。

Nuxt3はすごいですね。Netlifyでもサーバーリクエストを実現できています。

もうNetlify Functionsは使用しなくてもNuxt3だけで事足りるかもしれません。

最後に、Nuxt3の新しいサーバーエンジンをご紹介して本記事を終わります。

Nuxt3の新しいサーバーエンジン

Nuxt3ではNitro Engine(ニトロ エンジン)という新しいサーバーエンジンが採用されています。

Nitroサーバーの基盤は、rollupとh3である:高いパフォーマンスと移植性のために構築された最小限のhttpフレームワークである。

本番環境では、アプリとサーバーを1つのユニバーサルな .output ディレクトリに構築します。この出力は軽量で、minifyされ、あらゆるNode.jsモジュール(polyfillsを除く)から削除されています。この出力は、Node.js、サーバーレス、Workers、エッジサイドレンダリング、または純粋に静的なものまで、JavaScriptをサポートするあらゆるシステムでデプロイできます。

引用: Nuxt3

Nitro Engineは、JAMstackに最適化されたスタンドアロン(他のリソースに依存せず単独で機能する)サーバーです。

スタンドアロンサーバー
Nitro は node_modules に依存しないスタンドアロンなサーバ dist を生成します。

Nuxt 2のサーバはスタンドアロンではなく、nuxt startの実行(nuxt-startやnuxtディストリビューションを使用)やカスタムプログラムによる使用など、nuxt coreの一部が関与する必要があり、壊れやすくサーバレスやサービスワーカーの環境には適さないものでした。

このdistは、nuxt buildを実行すると、.outputディレクトリに生成されます。

出力はランタイムコードの両方と組み合わされ、あらゆる環境でNuxtサーバーを実行し(実験的なブラウザサービスワーカーを含む!)、静的ファイルを提供し、JAMstackのための真のハイブリッドフレームワークとなります。さらに、ネイティブストレージレイヤーが実装されており、マルチソース、ドライバ、ローカルアセットをサポートしています。

引用: Nuxt 3 - Server Engine

Nuxt2に存在したnuxt.config.jstargetプロパティは、Nuxt3のドキュメントから削除されました。プロパティ自体は引き続き使用可能です。

もうtarget: 'static'を定義しなくても、Nuxt3がJAMstackに最適化された静的ファイルを生成してくれるんですね。

引き続きssrプロパティは存在します。

参考: Nuxt 3 - Nuxt configuration file

あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる。そんな方に向けた単発・短期間メンターサービスを行っています。
独学プログラマのサービス
独学プログラマ
独学でも、ここまでできるってよ。
CONTACT
Nuxt.js制作のご依頼は下記メールアドレスまでお送りください。