🏬

BASE APIを使ってショップ情報や商品情報を取得してみる

2024/05/21に公開

はじめに

僕はオンラインショップで本屋さんを営んでおります。

玉葱堂書店

その母体となるのが BASE というサービスで、他にも有名なところでは STORES がありますが、僕はたまたま BASE で始めたので、今も使い続けています。

BASE Developers | BASE

特に何の問題もなく使えているのですが、どうしても外部からショップデータを取得してみたいという思いが高まり、ついつい検索してみたら、上記のリンクのように BASE API が Developer に向けて公開されていました。

そちらを使って、ショップ情報を取得してみたいと思います。

ググってもあまり記事はなく、あっても PHP のサンプルコードぐらいなので、 TypeScript and Next.js 版を書いてみます。

かなり限定的な方向けな記事になりますが、参考になれば幸いです。

ちなみに無料で使える API です。

全体のコードは GitHub に置いておきます。

hisasann/base-api-tamanegido

BASE APIで何が取得できるのか?

全貌はこの はじめに のリンク先にありますが、ざっくりと以下のようなものが取得できます。

はじめに · Developers

認証

OAuth2.0に対応しています。

なので、リフレッシュトークンやアクセストークンという概念が出てきます。

取得できるもの

  • ショップ情報を取得
  • 商品情報の一覧を取得
  • 商品情報を更新
  • 商品情報を削除
  • カテゴリー情報の一覧を取得
  • 注文情報の一覧を取得
  • 検索はどうやら新規受付は終了しているようなので利用できなさそうです

APIの利用制限

現在はユーザーの1時間の利用上限を5000回、1日の利用上限を100000回に設定しています。

これぐらいなら開発中にリクエストしても、すぐに上限に達することはなさそうですね。

外部サイトからこれらの情報にアクセスする際には、この上限だけ気にしておけば良いかと思います。

これで、自分でデザインしたサイトに自分のショップの商品情報を表示することができるようになります。

BASE APIを使う方法

BASE APIを申請する

BASE Developers

こちらのリンクからまずは申請します。

僕の場合は、申請ボタンを押したらすぐに承認されました。

他のサイトには1週間とか待つみたいな情報もあったのですが、このタイミングでは一瞬で OK そうでした。

あとはメールも来ました。

アプリ名アプリの説明 はそれっぽい感じで入力しました。

アプリURLコールバックのURL は同じ URL を指定しました。

アプリの画像は不要のようです。

利用権限は全部にチェックをつけておきました。

開発者情報 のところは必須のところだけ入力しました。

申請後は、 client_idclient_secret が発行されるので、それを使って API を叩いていきます。

なので、メモしておきましょう!

認可コードを取得する

流れとしては 認可コード を取得し、それを使って リフレッシュトークン を取得します。

そして、1時間という有効期限の アクセストークン を取得します。

まずは認可コードです。

以下のリンクに書かれています。

oauth/authorize · Developers

ここはブラウザでアクセスするでも curl でも良さそうです。

本当はこの認可コードを取得する部分まで全自動にしようかと思ったのですが、メールアドレスに確認コードが送られてそれを入力して初めてログインができるため、全自動が厳しいと判断したので、手動で取得することにしました。

puppeteer を使ったイメージはこんな感じでした。(今回は不採用)

const scrape = async () => {
  const browser = await puppeteer.launch({
    headless: true,
    slowMo: 100,
    defaultViewport: null,
  });

  const page = await browser.newPage();

  const authUrl = `${BASE_API_URL}/1/oauth/authorize?response_type=code&client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}&redirect_uri=${process.env.CALLBACK}&scope=read_users%20read_orders&state=hogehoge`;
  await page.goto(authUrl, { waitUntil: 'domcontentloaded' });

  await page.type('#UserMailAddress', process.env.EMAIL);
  await page.type('#UserPassword', process.env.PASSWORD);
  const target = '.submitBtn';
  const results = await page.$$eval(target, (elements) =>
    elements.map((element) => element.value),
  );
  console.log(results);
  await page.click(target);

  await browser.close();
};

有効期限は30日なので、30日ごとにこの URL を叩いて認可コードを取得し直す必要があります。

URL はこんな感じです。

https://api.thebase.com/1/oauth/authorize?response_type=code&client_id=取得したもの&redirect_uri=指定したもの&scope=read_users%20read_orders&state=hogehoge

client_id と redirect_uri は取得したものを使ってください。

この URL にアクセスすると、 redirect_uri にリダイレクトされるので、その URL に認可コードが付いてきます。

プログラムは以下のようになります。

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  // GET /1/oauth/authorize
  const code = searchParams.get('code');
  console.log('code', code);
  if (!code) {
    return NextResponse.json({
      message: `code がありません`,
    });
  }

  try {
    await fsPromises.writeFile(
      CODE_DATA_FILE_PATH,
      JSON.stringify({ code }),
      'utf8',
    );
  } catch (e) {
    console.error('writeFile code Error: ' + e);
  }

  // NOTE: /1/oauth/authorize はたとえ puppeteer でスクレイピングしても、二段階認証が発動し、メールでの認証が挟むため、完全に自動化するのは不可能だった
  // await scrape();

  return NextResponse.json({
    message: `${code} コールバックされました`,
  });
}

ローカルでは開発しやすいように、

export CODE=取得したコード

のようにターミナルに打ち込んで、環境変数としてロードしています。

僕は最近は、 API を雑に趣味で開発する時は Next.js の API 機能を使っています。

create-next-app ですぐに環境を作れるので、便利ですね。

リフレッシュトークンを取得する

リフレッシュトークンを取得するためには、認可コードが必要です。

それを使ってリフレッシュトークンを取得して保存するコードは以下のようになります。

import fsPromises from 'fs/promises';

import {
  ERROR_MESSAGE,
  REFRESH_TOKEN_DATA_FILE_PATH,
} from '@/app/api/constants';
import { fetchRefreshToken } from '@/app/api/sample/pkg/fetchRefreshToken';
import { RefreshTokenResponse } from '@/app/api/sample/pkg/type';

const _getToken = async (code: string): Promise<RefreshTokenResponse> => {
  let refreshTokenJson: RefreshTokenResponse = { refresh_token: '' };
  try {
    // refreshToken がない場合は code を使って取得する
    refreshTokenJson = await fetchRefreshToken(code);

    // refreshToken が取得できた場合は保存する
    if (refreshTokenJson.refresh_token) {
      // refreshToken を保存する
      await fsPromises.writeFile(
        REFRESH_TOKEN_DATA_FILE_PATH,
        JSON.stringify({ ...refreshTokenJson }),
        'utf8',
      );
    }
  } catch (e) {
    // @ts-ignore
    throw new Error(e);
  }

  // code の有効期限が切れている場合
  if (
    refreshTokenJson.error &&
    refreshTokenJson.error_description === ERROR_MESSAGE.CODE_EXPIRED
  ) {
    console.log('code 有効期限切れ!');
    throw new Error(ERROR_MESSAGE.CODE_EXPIRED);
  }

  return refreshTokenJson;
};

export const getRefreshToken = async (
  code: string,
): Promise<RefreshTokenResponse> => {
  console.log('getRefreshToken code:', code);
  let data = null;

  try {
    const refreshTokenJsonData = await fsPromises.readFile(
      REFRESH_TOKEN_DATA_FILE_PATH,
      'utf8',
    );
    data = JSON.parse(refreshTokenJsonData);
    console.log('refreshTokenJsonData あった!');
  } catch (e) {
    console.error('getRefreshToken Error: ' + e);
    // refreshToken がない場合は code を使って取得する
    data = await _getToken(code);
  }

  console.log('getRefreshToken:', data);
  return data;
};

実際にリフレッシュトークンを通信して取得するコードは以下のようになります。

import { BASE_API_URL } from '@/app/api/constants';
import { RefreshTokenResponse } from '@/app/api/sample/pkg/type';

/**
 * Fetches the refresh token from the base API using the authorization code.
 *
 * @param {string | undefined} code - The authorization code.
 * @returns {Promise<string>} - The refresh token.
 */
export const fetchRefreshToken = async (
  code: string | undefined,
): Promise<RefreshTokenResponse> => {
  let json: RefreshTokenResponse = { refresh_token: '' };

  const response = await fetch(
    `${BASE_API_URL}/1/oauth/token?grant_type=authorization_code&client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}&code=${code}&redirect_uri=${process.env.CALLBACK}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    },
  );

  json = await response.json();
  if (!response.ok) {
    new Error('HTTP-Error: ' + response.status + ' ' + response.statusText);
  }
  // console.log('fetchRefreshToken:', json);

  return json;
};

アクセストークンを取得する

アクセストークンはリフレッシュトークンを取得するコードとほぼ同じような感じです。

import fsPromises from 'fs/promises';

import {
  ACCESS_TOKEN_DATA_FILE_PATH,
  ERROR_MESSAGE,
} from '@/app/api/constants';
import { fetchAccessToken } from '@/app/api/sample/pkg/fetchAccessToken';
import { AccessTokenResponse } from '@/app/api/sample/pkg/type';

const _getToken = async (
  refreshToken: string,
): Promise<AccessTokenResponse> => {
  let accessTokenJson: AccessTokenResponse = { access_token: '' };
  try {
    // accessToken がない場合は code を使って取得する
    accessTokenJson = await fetchAccessToken(refreshToken);

    // accessToken が取得できた場合は保存する
    if (accessTokenJson.access_token) {
      // accessToken を保存する
      await fsPromises.writeFile(
        ACCESS_TOKEN_DATA_FILE_PATH,
        JSON.stringify({ ...accessTokenJson }),
        'utf8',
      );
    }
  } catch (e) {
    // @ts-ignore
    throw new Error(e);
  }

  // code の有効期限が切れている場合
  if (
    accessTokenJson.error &&
    accessTokenJson.error_description === ERROR_MESSAGE.REFRESH_TOKEN_EXPIRED
  ) {
    console.log('refreshToken 有効期限切れ!');
    throw new Error(ERROR_MESSAGE.REFRESH_TOKEN_EXPIRED);
  }

  return accessTokenJson;
};

export const getAccessToken = async (
  refreshToken: string,
): Promise<AccessTokenResponse> => {
  console.log('getAccessToken refreshToken:', refreshToken);
  let data = null;

  try {
    const accessTokenJsonData = await fsPromises.readFile(
      ACCESS_TOKEN_DATA_FILE_PATH,
      'utf8',
    );
    data = JSON.parse(accessTokenJsonData);
    console.log('accessTokenJsonData あった!');
  } catch (e) {
    console.error('getAccessToken Error: ' + e);
    // accessToken がない場合は refreshToken を使って取得する
    data = await _getToken(refreshToken);
  }

  console.log('getAccessToken:', data);
  return data;
};

ショップの情報を取得してみる

import { BASE_API_URL } from '@/app/api/constants';

/**
 * Retrieves user data from thebase.in API.
 *
 * @async
 * @param {string} access_token - The access token for authentication
 * @returns {Promise<Object>} - The JSON response containing user data
 */
export const fetchMe = async (
  access_token: string | undefined,
): Promise<object> => {
  let json = {};
  try {
    const response = await fetch(`${BASE_API_URL}/1/users/me?`, {
      method: 'GET',
      headers: {
        // https://note.com/pisuke2525/n/n00dc1a20efd3#b87fa57b-71aa-4b53-82a3-a99d41f351a7
        Authorization: 'Bearer ' + access_token,
      },
    });

    json = await response.json();
    if (!response.ok) {
      console.error('HTTP-Error: ' + response.status, response.statusText);
    }
  } catch (e) {
    // @ts-ignore
    throw new Error(e);
  }
  console.log('fetchMe:', json);

  return json;
};

商品一覧を取得してみる

import { BASE_API_URL } from '@/app/api/constants';

/**
 * Fetches items from the API using the provided access token.
 *
 * @param {string | undefined} access_token - The access token required to make the API request.
 * @returns {Promise<object>} - A promise that resolves to the fetched items as a JSON object.
 */
const fetchItems = async (
  access_token: string | undefined,
): Promise<object> => {
  let json = {};
  try {
    const response = await fetch(`${BASE_API_URL}/1/items?`, {
      method: 'GET',
      headers: {
        // https://note.com/pisuke2525/n/n00dc1a20efd3#b87fa57b-71aa-4b53-82a3-a99d41f351a7
        Authorization: 'Bearer ' + access_token,
      },
    });

    json = await response.json();
    if (!response.ok) {
      console.error('HTTP-Error: ' + response.status, response.statusText);
    }
  } catch (e) {
    // @ts-ignore
    throw new Error(e);
  }
  console.log('fetchItems:', json);

  return json;
};

注文情報を取得してみる

import { BASE_API_URL } from '@/pkg/constants';

/**
 * Fetches orders from the API using the provided access token.
 *
 * @param {string | undefined} access_token - The access token for authentication.
 * @returns {Promise<object>} - A Promise that resolves to the JSON response containing the orders data.
 */
const fetchOrders = async (
  access_token: string | undefined,
): Promise<object> => {
  let json = {};
  try {
    const response = await fetch(`${BASE_API_URL}/1/orders?`, {
      method: 'GET',
      headers: {
        // https://note.com/pisuke2525/n/n00dc1a20efd3#b87fa57b-71aa-4b53-82a3-a99d41f351a7
        Authorization: 'Bearer ' + access_token,
      },
    });

    json = await response.json();
    if (!response.ok) {
      console.error('HTTP-Error: ' + response.status, response.statusText);
    }
  } catch (e) {
    // @ts-ignore
    throw new Error(e);
  }
  // console.log('fetchOrders:', json);

  return json;
};

今回の成果物

hisasann/base-api-tamanegido

あくまでもローカルをベースにしているので、デプロイする際には環境変数を設定してください。

また、 Vercel はファイルを保存することができないので、データストアを別に設けるなどが必要になります。

今回はサンプルとして作っているので、その辺りはご容赦ください。

ハマりポイント

token を取得するリクエストは POST で行うのですが、 body 部分に送信したい情報を入れる方法はうまくいかず、あくまでも URL のクエリパラメータに入れる必要がありました。

ここは思いの外ハマりました。

ついつい POST なので、 body に入れるのかと思っていたのですが、そうではなかったです。

あとはアクセストークンを渡す方法は、はじめにのページにしか書いておらず、そこを見落としていたので、どうやって渡すのかわからなくなりました。

OAuth2.0 の渡し方なので、うろ覚えとしてはあったのですが、それを思い出すのに時間がかかりました。

実際には以下のように headers に渡します。

const response = await fetch(`${BASE_API_URL}/1/items?`, {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + access_token,
  },
});

おわりに

30日で認可コードの賞味期限が切れるので、途中からはただの検証になってしまいましたが、 BASE API を使ってショップ情報や商品情報を取得する方法を書いてみました。

商品が購入されたらそれを検知したいという思いがあったのですが、 push 型の何かがあるわけでもないので、やるとしたら order 情報をマスキングして保存しておいて、変化が生まれたら注文されたという状態にするとかですかね。

つまりクーロンのようなもので、回す必要がありそうです。

BASE API は無料で使えるので、自分のショップの情報を取得するのには最適だと思います。

あとは使い所ですねー。

ではでは。👍

参考記事

BASEのAPIでできること|使い方や申請・設定方法を解説【2024年最新版】 | システム幹事

BASE APIの使い方・商品データの取り出し方を解説する【PHPコード付き】

どなたかBase APIに詳しい方はおられないでしょうか?

ネットショップBASE(ベイス)のAPIを使って商品一覧を表示させてみた | WordPressの勉強がてら

BASE APIで商品データを取得 · GitHub

BASE APIとは?できることやネットショップサービスへの申請・連携方法を解説

BASEのAPIを使用して商品の売上げランキング一覧を作成する方法 | デジタル戦略の広告代理店 株式会社H2O

Discussion