Ribbit's works

TypeScriptで楽天ブックス書籍検索APIを使う📚

#TypeScript #JavaScript
にメンテナンス済み
記事のトップ画像

最近ツイッターもフェイスブックも、API を使用するための審査が厳しくなっているようですね。

申請にあたって色んな作文を書かないといけませんが、楽天 API は幸い、楽天のアカウントさえ持っていれば利用することができます。

今回はその中でも、楽天ブックス書籍検索 APIを使って、目的の本を取得する処理を作成したいと思います。

ちなみにこのサイトは静的サイトジェネレータを使って作成していますが、今回ご紹介するコードを使って、ビルド時に各記事に関連する本を自動取得する処理をいれています。

確認したい方は記事の一番下までどうぞ。

型定義

今回紹介するのは、他の API も含めてすべて1つの名前空間に収めてしまう方法を使っていますが。ご自由に分解して使ってください。

/** 複数のうち最低1つが必須となっているパラメータ用 */
type RequireOne<T, K extends keyof T = keyof T> = K extends keyof T ? PartialRequire<T, K> : never;
type PartialRequire<O, K extends keyof O> = {
  [P in K]-?: O[P];
} & O;

export namespace rakuten {
  export namespace api {
    /** ソートに指定できる文字列 */
    export type Sorting =
      | '+reviewCount'
      | '-reviewCount'
      | '+reviewAverage'
      | '-reviewAverage'
      | '+itemPrice'
      | '-itemPrice'
      | '+updateTimestamp'
      | '-updateTimestamp'
      | 'standard';

    export namespace books {
      type Auth = {
        applicationId: string;
        affiliateId?: string;
      };

      type Options = {
        /** デフォルト: json */
        format?: 'json' | 'xml';
        callback?: string;
        elements?: (keyof Book)[];
        /** デフォルト: 1 */
        formatVersion?: 1 | 2;
        title?: string;

        /** 1から30まで。デフォルトは30 */
        hits?: number;
        page?: number;
        availability?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
        outOfStockFlag?: 0 | 1;
        chirayomiFlag?: 0 | 1;
        sort?: Sorting;
        limitedFlag?: 0 | 1;
        carrier?: number;
        genreInformationFlag?: 0 | 1;
      };

      type EitherRequired = RequireOne<{
        title?: string;
        author?: string;
        publisherName?: string;
        size?: number;
        isbn?: string;
        booksGenreId?: string;
      }>;

      export type Request = Auth & Options & EitherRequired;

      export type RequestWithoutAuth = Options & EitherRequired;

      /** フォーマットバージョン2 */
      export type Book = {
        limitedFlag: 0 | 1;
        authorKana: string;
        author: string;
        subTitle: string;
        seriesNameKana: string;
        title: string;
        subTitleKana: string;
        itemCaption: string;
        publisherName: string;
        listPrice: 0;
        isbn: string;
        largeImageUrl: string;
        mediumImageUrl: string;
        titleKana: string;
        availability: '1';
        postageFlag: 2;
        salesDate: string;
        contents: string;
        smallImageUrl: string;
        discountPrice: 0;
        itemPrice: number;
        size: string;
        booksGenreId: string;
        affiliateUrl: string;
        seriesName: string;
        reviewCount: number;
        reviewAverage: string;
        discountRate: 0;
        chirayomiUrl: string;
        itemUrl: string;
      };

      export type Response = { Items: Book[] };
    }
  }
}

API には formatVersion というパラメータがあり、これを変更することで返ってくる JSON の構造が変わります。

バージョンには 1 と 2 がありますが、2 の方が使いやすいので、今回は 2 を必ず指定することを前提として定義してます。

基本的には公式ドキュメントの通り型を定義しただけですが、TypeScript の恩恵を受けやすくするため、よく利用する elements パラメータだけは型を変更しているので注意してください。

API のエンドポイントに負担をかけないためにも、仕様が固まったら elements パラメータは最小限で指定するようにしましょう。

API クラス

import axios from 'axios';
import { RakutenAPIClient } from '.';
import { rakuten } from './types';

const DOMAIN = 'https://app.rakuten.co.jp/';

export class RakutenBooksClient {
  public static END_POINT = 'services/api/BooksBook/Search/20170404';

  public async get(request: rakuten.api.books.Request) {
    const url = this.getUrl(request);
    const response = await axios.get<rakuten.api.books.Response>(url);
    return response;
  }

  private getUrl(request: rakuten.api.books.Request) {
    let url = RakutenAPIClient.DOMAIN + RakutenBooksClient.END_POINT;
    url += `?applicationId=${request.applicationId}`;
    url += `&affiliateId=${request.affiliateId}`;
    url += `&formatVersion=2`;

    if (request.callback) {
      url += `&callback=${request.callback}`;
    }
    if (request.elements) {
      url += `&elements=${request.elements.join(',')}`;
    }

    if (request.title) {
      url += `&title=${encodeURIComponent(request.title)}`;
    }
    if (request.author) {
      url += `&author=${encodeURIComponent(request.author)}`;
    }
    if (request.publisherName) {
      url += `&publisherName=${encodeURIComponent(request.publisherName)}`;
    }
    if (request.size) {
      url += `&size=${request.size}`;
    }
    if (request.isbn) {
      url += `&isbn=${request.isbn}`;
    }
    if (request.booksGenreId) {
      url += `&booksGenreId=${request.booksGenreId}`;
    }
    if (request.hits) {
      url += `&hits=${request.hits}`;
    }
    if (request.page) {
      url += `&page=${request.page}`;
    }
    if (request.availability) {
      url += `&availability=${request.availability}`;
    }
    if (request.outOfStockFlag) {
      url += `&outOfStockFlag=${request.outOfStockFlag}`;
    }
    if (request.chirayomiFlag) {
      url += `&chirayomiFlag=${request.chirayomiFlag}`;
    }
    if (request.sort) {
      url += `&sort=${request.sort}`;
    }
    if (request.limitedFlag) {
      url += `&limitedFlag=${request.limitedFlag}`;
    }
    if (request.carrier) {
      url += `&carrier=${request.carrier}`;
    }
    if (request.genreInformationFlag) {
      url += `&genreInformationFlag=${request.genreInformationFlag}`;
    }

    return url;
  }
}

ほぼ条件式ですね。

今回はメンバー変数が1つもありませんが、認証情報を予め指定しても良いと思います。

export class RakutenBooksClient {
  public static END_POINT = 'services/api/BooksBook/Search/20170404';

  private readonly _applicationId: string;
  private readonly _applicationSecret: string;
  private readonly _affiliateId: string;

  public constructor(applicationId: string, applicationSecret: string, affiliateId: string) {
    this._applicationId = applicationId;
    this._applicationSecret = applicationSecret;
    this._affiliateId = affiliateId;
  }

  //...

使い方の例

import { RakutenBooksClient } from './rakuten-books-client';

(async () => {
  const client = new RakutenBooksClient();

  const response = await client.get({
    applicationId: '____ここに取得したアプリIDを設定____',
    title: 'こんにちは',
    hits: 4,
    availability: 1,
    elements: ['title', 'author', 'largeImageUrl', 'itemPrice', 'affiliateUrl'],
  });

  console.log(response);
})();