YouTube Data APIを使って検索結果を表示する(Node.js, Express.js, React.js, TypeScript)

API

今回はYouTube Data APIを使って、特定のキーワードの検索結果を表示するだけのWebアプリを作ってみます。

参考のためにGithubにコードを上げています

Google CloudコンソールにてAPIキーを発行する

最初に、YouTube Data APIを利用するためのAPIキーを発行します。

プロジェクトの新規作成

Google Cloudのコンソールにログインして、新しくプロジェクトを作成します。
(ちなみに設定で日本語表示に変更もできます。)

ロゴの横の下三角をクリックするとポップアップが出るので、NEW PROJECTをクリック
ロゴの横の下三角をクリックするとポップアップが出るので、NEW PROJECTをクリック

プロジェクト名は「Using YouTube API」としました。

APIキーの発行

次に、左上のハンバーガーメニューをクリックして、APIs & ServicesのCredentialsを選択します。

APIs & ServicesのCredentialsを選択
APIs & ServicesのCredentialsを選択

異動したページの上の方にある、+ CREATE CREDENTIALSで API Keyを選択すると自動的にAPIキーが生成されます。

APIキーが生成されました。

このキーは後で使います。キーの右側のアイコンを押すとコピーされます。

APIキーは公開NGです。ダメ、絶対。

リクエストURLの取得

次は、YouTube Data APIのリファレンスを参考に、検索用のリクエストURLを取得します。

YouTube Data APIリファレンスのSearch: listのページで、右側にリクエストを試すためのフォームがあるのでそれを使用します。

今回使用するパラメーターと値を表にまとめてみました。

パラメーター説明
partsnippetリファレンスに書いてあるのでsnippetと入力
maxResults50取得アイテムの最大数
q料理検索ワード。ここでは「料理」とする
relevanceLanguageja日本語の動画を検索
typevideoチャンネルやプレイリストではなく動画を検索
使用するパラメーターと値

他は空欄でOKです。

入力したらCredentialsをAPI Keyだけにチェックを入れてSHOW CODEを押します。

ここではGoogle OAuth 2.0はチェックを外す
ここではGoogle OAuth 2.0はチェックを外す

するとポップアップでcURLのコードが表示されます。
横のHTTPのタブを見てもOKです。

https://youtube~以降のURLを使用します。

https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=50&q=%E6%96%99%E7%90%86&relevanceLanguage=ja&type=video&key=[YOUR_API_KEY]

[YOUR_API_KEY]の部分に、先程取得したAPIキーを入れるわけですね。

バックエンド側の作成

YouTube Data APIへのリクエストURLは準備できましたので、バックエンド側のコードを書いていきましょう。

必要なパッケージのインストール

適当なフォルダを用意して、

npm init -y

でpackage.jsonファイルを作成します。

続いて、

npm install --save-dev dotenv nodemon
npm install express axios

で必要なパッケージをインストールします。

各種設定

.env

次は、ルートディレクトリに.envという名前のファイルを作成して下記を記入します。
package.jsonと同じディレクトリです。

PORT=5000
API_KEY=xxxxxx

xxxxxのところは上の方で取得したGoogle CloudのAPIキーをコピペしてください。

.gitignore

APIキーがGithub等で公開されてしまわないように、念のため、新しく.gitignoreというファイルを作成して下記を記載しておきます。

node_modules
.env

package.json

先程生成されたpackage.jsonファイルにて、scripts内に下記を追加します。

"dev": "nodemon -r dotenv/config server.js"

こうすることで.envファイルを自動的に読み込みます。
nodemonを使うことで、ファイルを保存するたびにサーバーが再起動されるようになります。

あと、mainをserver.jsに変更します。
nameは適当にyoutube-data-apiとしました。

package.jsonの全体はこうなっています。

{
  "name": "youtube-data-api",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "dev": "nodemon -r dotenv/config server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "dotenv": "^16.0.3",
    "nodemon": "^2.0.20"
  },
  "dependencies": {
    "axios": "^1.3.3",
    "express": "^4.18.2",
    "morgan": "^1.10.0"
  }
}

server.jsを作成してコードを記載

ルートディレクトリにserver.jsというファイルを作成して中身を埋めます。
全体のコードは、こうです。

const express = require("express");
const app = express();
const port = process.env.PORT;
const axios = require("axios");
const morgan = require("morgan");

const apiKey = process.env.API_KEY;
const query = "料理";
const requestUrl = `https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=50&q=${query}&relevanceLanguage=ja&type=video&key=${apiKey}`;

app.use(morgan("dev"));

app.get("/api", (req, res) => {
  axios
    .get(requestUrl)
    .then((response) => {
      console.log(response.data);
      res.send(response.data);
    })
    .catch((error) => {
      console.log(error);
      res.status(400).send(error);
    });
});

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

コードの説明

基本的にExpress.jsの書き方そのまんまです。

process.envで、.envファイルにアクセスします。
それぞれportとapiKeyという変数に格納しています。

morganはメンテナンス用に使用しています。

requestUrlは、先程確認したYouTube Data APIへのリクエストURLを指定します。
末尾にAPIキーを付けるのを忘れずに。

あと、検索ワードだけは別に変数を用意しました。
(const query = “料理”;)

エンドポイントの”/api”にGETリクエストがあると、axiosを使ってrequestUrlにGETリクエストを出します。

レスポンスがあればレスポンスを返送し、エラーであればエラーを返送します。

動作の確認

GETリクエストだけなので、ブラウザで動作を確認してみます。

まだサーバーを起動していなければ、ターミナルで、

npm run dev

を実行してサーバーを起動します。

無事に起動されたら、http://localhost:5000/apiにアクセスします。

JSONが返ってきました。
JSONが返ってきました。

問題なければYouTubeの検索結果のJSONデータが表示されるはずです。
(JSON FormatterというChrome拡張機能を使っています)

バックエンドは以上です。

余談:ここでいったんgit init

次で使うCreate React Appは、プロジェクトフォルダ内に.gitがなければ新しく.gitファイルを作成してしまうため、後でめんどくさくなります。(なりました)

バックエンド・フロントエンドを合わせてGitで管理したい場合は、遅くともこのタイミングでgit initをしておいたほうがいいです。

フロントエンド側、クライアントの作成

次に、JSONデータを受けて表示するフロントエンド側、クライアントを作成します。

create-react-appを実行

バックエンドで使用したフォルダのルートディレクトリで、create-react-appを実行します。
今回はTypeScriptで記述したいのでAdding TypeScriptのページを参考にします。

npx create-react-app client --template typescript

フォルダ名はclientにしました。

インストールにはしばらく時間がかかります。
インストールが完了したら、

cd client
npm start

でReactアプリを実行します。

プロキシの設定

Reactアプリが無事に動作したら、バックエンドとフロントエンドの開発を簡単にするために、Create React Appのプロキシ設定を行っておきます。

Create React AppのProxying API Requests in Developmentのページを参考に、package.jsonに追記します。

Client側のpackage.jsonです。

{
  "name": "client",

  <間は省略>
  "proxy": "http://localhost:5000"
}

こうしておくことで、開発中に、例えば”/api”にアクセスすると、”http://localhost:5000/api”を参照するようになります。

Itemsコンポーネントの作成

それでは、Itemコンポーネントを作成していきます。

client内のsrc以下に、featuresというフォルダを作って、Items.tsxとItems.cssというファイルを作成します。

/client/
 - public/
 - src/
  - features/
   - Items.tsx
   - Items.css
以下略  

Items.tsx

Items.tsxの中身は下記です。

import { useState, useEffect } from "react";
import "./Items.css";

function Items() {
  interface Data {
    items?: Item[];
  }
  interface Item {
    id: { videoId: string };
    snippet: {
      title: string;
      description: string;
      publishedAt: string;
      thumbnails: {
        medium: {
          url: string;
        };
      };
      channelTitle: string;
      channelId: string;
    };
  }

  const [data, setData] = useState<Data>({});
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [error, setError] = useState<Error | null>(null);

  const requestUrl = "/api";
  const youtubeUrl = "https://www.youtube.com/watch?v=";
  const channelUrl = "https://www.youtube.com/channel/";

  const formatDate = (publishedAt: string) => {
    const date = new Date(publishedAt);
    return date.toLocaleString("ja-JP");
  };

  useEffect(() => {
    fetch(requestUrl)
      .then((res) => {
        return res.json();
      })
      .then(
        (res) => {
          setIsLoading(false);
          if (!res.items) {
            setError(res);
          }
          setData(res);
        },
        (error) => {
          setIsLoading(false);
          setError(error);
        }
      );
  }, []);

  if (error) {
    return <div className="error">Error: {error.message}</div>;
  }
  if (isLoading) {
    return <div className="loading">Loading...</div>;
  }
  return (
    <section className="items">
      {data.items?.map((item: Item, i: number) => {
        return (
          <div className="item" key={i}>
            <div className="thumbnail">
              <a href={youtubeUrl + item.id.videoId}>
                <img
                  src={item.snippet.thumbnails.medium.url}
                  alt={item.snippet.title}
                />
              </a>
            </div>
            <div className="right">
              <div className="title">
                <a href={youtubeUrl + item.id.videoId}>{item.snippet.title}</a>
              </div>
              <div className="description">{item.snippet.description}</div>
              <div className="channel">
                <a href={channelUrl + item.snippet.channelId}>
                  {item.snippet.channelTitle}
                </a>
              </div>
              <div className="time">{formatDate(item.snippet.publishedAt)}</div>
            </div>
          </div>
        );
      })}
    </section>
  );
}

export default Items;

ひとつのファイルに詰め込んだら長くなってしまいました。

Items.tsxの説明

まずは準備から。

interface Dataと、interface Itemはそれぞれのオブジェクトの型を指定しています。

useState()を使用して、取得するデータ(data)と、ロード中か否か(isLoading)、エラーが有るか否か(error)を保持します。

requestUrlは、proxyのところで書いたように、”http://localhost:5000/api”を指します。

youtubeUrlとchannelUrlはそれぞれ、YouTube動画とチャンネルのURLです。リンクを貼るときに使います。

formatDate()は、APIから取得する日時をフォーマットするために用意しました。

次は、APIにアクセスする部分です。

ReactのuseEffectを使って、ページが読み込まれたときにfecth()を実行します。

返ってきたデータを.json()でJavaScriptオブジェクトに変換して、それから(then)、dataにセットします。

このとき、isLodingをfalseにして、データ内にitemsプロパティがなければerrorをセットします。

その他のエラーがあれば、そのerrorをセットします。

次はJSXの表示部分です。

errorがあれば、errorのmessageプロパティの中身を表示します。
isLoadingがtrueであれば、Loading…というテキストを表示します。

エラーでもロード中でもなければ、取得したAPIの中身を表示します。
APIのデータを保持しているdataのitemsプロパティは配列なので、.map()を使ってリスト的にズラッと表示します。

map()内のitemは、interfaceで定義したItem型です。
第二引数のiはnumberで、自動的にひとつずつ増えるのでリスト内のkeyに使います。

あとはそれぞれ、タイトルだったり詳細だったり画像だったりを表示してリンクを張ったりしています。

youtubeUrlにvideoIdを組み合わせて動画のURLを作成しています。
同様に、channelUrlにchannelIdを組み合わせてチャンネルURLを作成しています。

Itemsコンポーネントはこんな感じです。

Appコンポーネントを編集

次にAppコンポーネントでItemsコンポーネントを読み込みます。

元のApp.tsxから要らないものを削除して、必要なものを書き込んだのが以下です。

import Items from "./features/Items";
import "./App.css";

function App() {
  return (
    <div className="App">
      <header>
        <h1>YouTube Data APIを使って「料理」動画をリスト表示</h1>
      </header>
      <Items />
    </div>
  );
}

export default App;

複雑なことはないです。
Itemsをimportして、<header>の下に<Items />を配置しているだけです。

CSSでスタイリング

CSSで体裁を整えます。

デフォルトのCSSを削除して下記のようにしました。

index.css
body {
  margin: 0;
  font-family: "Helvetica Neue", Arial, "Hiragino Kaku Gothic ProN",
    "Hiragino Sans", Meiryo, sans-serif;
  background-color: #fff;
}

マージンを0にするのと、フォントファミリーの指定だけです。

App.css
.App {
  max-width: 980px;
  margin: auto;
}

h1 {
  color: #333;
  margin: 20px 10px;
}

.Appのコンテンツの表示幅の指定と、見出しの調整です。

Items.css
a {
  color: #333;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

.item {
  padding: 20px;
  margin: 20px 10px;
  display: flex;
  border-radius: 5px;
  background-color: #fff;
  color: #666;
  box-shadow: 0 0 15px rgba(100, 100, 100, 0.2);
}

.right {
  margin-left: 20px;
}

.right div {
  margin-bottom: 10px;
}

.right .title {
  font-weight: bold;
  font-size: 1.5rem;
}

.error {
  color: #ff0000;
  background-color: #ffcccc;
  padding: 10px;
  border-radius: 5px;
}

アイテムをカード風に表示するために色々と書いています。

また、エラーの際(.error)は赤っぽい感じで表示するようにしています。

エラーの際の表示
エラーの際の表示

完成品

完成品はこんな感じです。

Loading...の後にAPIの内容を表示
Loading…の後にAPIの内容を表示

今回は、検索ワードをバックエンド側で「料理」に絞って作成しました。

機会があれば検索フォームありのものも書きたいところです。

buildやdeployは割愛します。

以上です。

Happy Coding!

コメント

タイトルとURLをコピーしました