RESAS API を用いた人口推移の可視化アプリ

2024/05/13

RESAS API を使用して、都道府県ごとの人口推移を可視化するアプリを作成しました。

きっかけ

  • 株式会社ゆめみ様の「ゆめみパスポートチャレンジ」でフロントエンドのコーディング試験として本アプリケーションを作成しました。(コーディング試験の内容はオープンに公開されています。)
  • これまでアルバイトで Next.js、React などを使用した Web アプリ開発経験はあったのですが、0 から Next.js でアプリケーションを作る経験がなかったので、この機会に学習を深めようと思って取り組みました。

アプリケーションの概要

  • RESAS-API から都道府県情報や、都道府県ごとの人口推移データを取得し、グラフで可視化するアプリケーションです。
    • RESAS-API について:利用登録をして API キーを取得する必要があります。取得した API キーを使用して、RESAS(地域経済分析システム)に掲載されているデータを API 経由で取得することができます。(RESAS-API は 2025 年 3 月 24 日に提供が終了します。)
  • 都道府県のチェックボックスにチェックを入れると、その都道府県の人口推移データを API 呼び出しにより取得し、グラフで表示します。

使用技術

フレームワーク Next.js (React)
プログラミング言語 TypeScript
グラフ描画ライブラリ Highcharts
スタイリング css-modules
フォーマッタ Prettier
リンター ESLint
テストツール Playwright
CI/CD GitHub Actions
デプロイ Vercel
その他 Husky, Lint-staged, asdf

Next.js

  • ルーティングシステムとして App Router を使用しました。
  • app ディレクトリ以下のファイル構成は以下のようになっています。
app
├── api             ## API routes
│   ├── population/composition/perYear/route.ts
│   └── prefectures/route.ts
├── components      ## コンポーネント
│   ├── molecules
│   │   ├── ChartPane.tsx
│   │   ├── CheckBoxPane.tsx
│   │   └── PopulationChart.tsx
│   └── organisms
│       ├── PopuTrackLogo.tsx
│       └── PopuTrackPane.tsx
├── hooks           ## Custom hooks
│   ├── useActivePopulationCompositionData.ts
│   ├── usePrefectures.ts
│   └── usePrefecturesPopulation.ts
├── lib/api.ts      ## API routes へのリクエスト処理
├── styles          ## CSS Modules
├── types/index.ts  ## 型定義
├── ui              ## フォント関連など
├── layout.tsx      ## レイアウト
└── page.tsx        ## ルート

環境変数

  • .env にて以下の環境変数を設定します。
  • Next.js ではサーバーサイドから process.env.API_KEY のように環境変数を参照することができます。
API_KEY= # Your API key
POPULATION_COMPOSITION_API_URL="https://opendata.resas-portal.go.jp/api/v1/population/composition/perYear"
PREFECTURES_API_URL="https://opendata.resas-portal.go.jp/api/v1/prefectures"

(注) 2025 年 3 月 24 日で RESAS-API は使用できなくなります。動作確認を行いたい場合は population-tracker-jp-api の README に従って設定を行ってください。

API routes

本アプリではクライアント側から直接 RESAS-API を呼び出すのではなく、中間に API Routes を挟んでリクエストを行っています。これは、クライアント側から API キーなどの情報を秘匿することでセキュリティを向上させるためです。

本アプリでは、都道府県情報と人口推移データを取得するための 2 つの API を作成しました。

  • api/prefectures/route.ts
    • 都道府県の名前と識別コードの一覧の情報を取得する API
  • api/population/composition/perYear/route.ts
    • 都道府県ごとの人口推移データを取得する API
    • ?cityCode=-&prefCode=1 のようにクエリパラメータを指定することで、都道府県ごとの人口推移データを取得することができます。

api/population/composition/perYear/route.ts は以下のようになっています。

import { z } from "zod";

const querySchema = z.object({
  prefCode: z.string().refine((val) => {
    const intValue = parseInt(val);
    return intValue >= 1 && intValue <= 47;
  }),
});

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const prefCode = searchParams.get("prefCode");

  try {
    querySchema.parse({ prefCode });
  } catch {
    return new Response("Invalid query parameter", { status: 400 });
  }

  return await fetch(
    `${process.env.POPULATION_COMPOSITION_API_URL}?cityCode=-&prefCode=${prefCode}`,
    {
      method: "GET",
      headers: {
        "X-API-KEY": process.env.API_KEY || "",
      },
    }
  );
}
  • クエリパラメータのバリデーションには zod を使用しており、prefCode が 1 から 47 の間の整数であることの確認を行っています。
    • バリデーションに失敗した場合は、ステータスコード 400 を返します。

lib/api.ts

  • API Routes へのリクエスト処理を行う関数は lib/api.ts にまとめています。
    • getPrefectures()
      • 都道府県の名前と識別コードの一覧を取得します。
    • getPrefecturePopulationCompositionData(prefecture)
      • 指定した都道府県の人口推移データを取得します。
      • API で取得したデータを整形して返します。
  • リクエスト失敗時には例外を発生させます。

Custom hooks

カスタムフックを作成し、ロジックを分離しています。

  • useActivePopulationCompositionData
    • 引数として、複数の都道府県の人口推移データを受け取ります。
    • 返り値は { activeKey, setActiveKey, activePrefecturesPopulationDataList }
      • activeKey は「総人口」「年少人口」などの現在選択されている人口構成を保持します。
      • setActiveKeyactiveKey を更新する関数です。
      • activePrefecturesPopulationDataList は現在選択されている人口構成のデータを保持します。
  • usePrefectures
    • useEffect を使用しており、初回レンダリング時に都道府県の一覧情報を取得します。
  • usePrefecturesPopulationData
    • 返り値は { updateCheckedPrefectures, prefecturesPopulationCompositionData }
      • updateCheckedPrefectures はチェックボックスの状態を更新する関数です。
      • prefecturesPopulationCompositionData は選択されている都道府県の人口推移データを保持します。

CSS Modules

本アプリでは CSS Modules を使用してスタイリングを行っています。 CSS Modules を使用することで、クラス名の衝突を防ぎ、コンポーネントごとに独立したスコープでスタイルを定義することができます。

例えば、page.tsx は次のようになっています。

import styles from "@/app/styles/page.module.css";
import React from "react";

import { PopuTrackLogo } from "./components/organisms/PopuTrackLogo";
import { PopuTrackPane } from "./components/organisms/PopuTrackPane";

const Page: React.FC = () => {
  if (!process.env.API_KEY) {
    throw new Error("API_KEY not found.");
  }

  return (
    <main className={styles.main}>
      <div className={styles.mainContainer}>
        <div className={styles.header}>
          <PopuTrackLogo />
        </div>
        <PopuTrackPane />
        <footer className={styles.footer}>
          <p>&copy; {new Date().getFullYear()} seelx3</p>
        </footer>
      </div>
    </main>
  );
};

export default Page;
  • import styles from "@/app/styles/page.module.css";page.module.css を読み込んでいます。
  • className={styles.main} のように、クラス名でスタイルを指定しています。

Highcharts

本アプリでは Highcharts を使用して人口推移のグラフを描画しています。

  • HighchartReact コンポーネントの options props にグラフの設定を渡して描画します。
    • グラフのタイトル、X 軸、Y 軸、系列、ツールチップの表示などを設定することができます。
PopulationChart.tsx
"use client";

import { PopulationDataList } from "@/app/types";
import React from "react";

import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import Accessibility from "highcharts/modules/accessibility";

if (typeof Highcharts === "object") {
  Accessibility(Highcharts);
}

const TEN_THOUSAND = 10000;

const yAxisLabelFormatter = (value: number): string => {
  return Number(value) / TEN_THOUSAND + "万";
};

const populationFormatter = (value: number): string => {
  return (
    (value >= TEN_THOUSAND
      ? Math.floor(Number(value) / TEN_THOUSAND) + "万"
      : "") +
    (value % TEN_THOUSAND)
  );
};

type Props = {
  populationDataList: PopulationDataList;
  activeKey: string;
};

export const PopulationChart: React.FC<Props> = ({
  populationDataList,
  activeKey,
}) => {
  const categories: string[] = populationDataList[0]?.populationData.map(
    (populationData) => populationData.year.toString()
  );
  const series: Highcharts.SeriesOptionsType[] = populationDataList.map(
    (data) => ({
      type: "line",
      name: data.prefName,
      data: data.populationData.map((populationData) => populationData.value),
    })
  );

  const options: Highcharts.Options = {
    title: {
      text:
        activeKey === "total"
          ? "総人口の推移"
          : activeKey === "young"
          ? "年少人口の推移"
          : activeKey === "productive"
          ? "生産年齢人口の推移"
          : activeKey === "elderly"
          ? "老年人口の推移"
          : "",
    },
    xAxis: {
      title: {
        text: "年度",
      },
      categories,
    },
    yAxis: {
      title: {
        text: "人口数",
      },
      labels: {
        formatter: function () {
          return yAxisLabelFormatter(Number(this.value));
        },
      },
    },
    series,
    accessibility: {
      description: "都道府県別の人口推移を示すグラフ",
    },
    tooltip: {
      formatter: function () {
        return (
          "<b>" +
          this.series.name +
          "</b><br>" +
          this.x +
          "年<br>" +
          populationFormatter(this.y as number) +
          "人"
        );
      },
    },
  };

  return <HighchartsReact highcharts={Highcharts} options={options} />;
};

Playwright

本アプリでは Playwright を使用して E2E テストを行っています。

tests/e2e/popuTrack.spec.ts では以下のようなことをテストすることができます。

  • タイトルが「都道府県別人口推移」である
  • 都道府県のチェックボックスが 47 個存在する
  • 東京都のチェックボックスのチェックを切り替えたときのグラフの振る舞いが正しい
  • 複数の都道府県を選択したときのグラフの振る舞いが正しい
  • 人口構成のタブを切り替えたときのグラフの振る舞いが正しい

例えば、「東京都のチェックボックス切り替え」のテストは次のようにしてテストします。

import { expect, test } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.goto("http://127.0.0.1:3000");
});

test("東京都のチェックボックスのチェックの切り替え", async ({ page }) => {
  // 東京都のチェックボックスをクリックする
  await page.check("#東京都");

  // 東京都のチェックボックスがチェックされている
  await expect(page.locator("#東京都")).toBeChecked();

  // Highcharts に折れ線グラフが 1 本描画されている
  await expect(page.locator(".highcharts-tracker-line")).toHaveCount(1);

  // Highcharts の凡例に "東京都" の 1 つだけが表示されている
  const legendItem = page.locator(".highcharts-legend-item");
  await expect(legendItem).toHaveCount(1);
  await expect(legendItem).toHaveText("東京都");

  // 東京都のチェックボックスのチェックを外す
  await page.uncheck("#東京都");

  // Highcharts の折れ線グラフがない
  await expect(page.locator(".highcharts-tracker-line")).toHaveCount(0);

  // Highcharts の凡例がない
  await expect(page.locator(".highcharts-legend-item")).toHaveCount(0);
});

本プロジェクトでは yarn test-e2e:chromium で chromium ブラウザ環境で E2E テストを実行することができます。

GitHub CI

GitHub Actions で CI を構築しています。 GitHub 上で最新のコードが自動的にテストされることで、バグを早期に発見しやすくなります。

本プロジェクトでは、プッシュ時に Playwright によるテストを実行しています。

.github/workflows/playwright.yml で以下のように設定しています。

name: Playwright Tests
on: push
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm install -g yarn && yarn
      - name: Install Playwright Browsers
        run: yarn playwright install --with-deps
      - name: Run Playwright tests
        env:
          API_KEY: ${{ secrets.API_KEY }}
          PREFECTURES_API_URL: ${{ secrets.PREFECTURES_API_URL }}
          POPULATION_COMPOSITION_API_URL: ${{ secrets.POPULATION_COMPOSITION_API_URL }}
        run: yarn test-e2e:chromium
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

GitHub Actions のテスト内で環境変数を使用するため、GitHub のリポジトリの「Settings」->「Secrets and variables」から環墫変数を設定する必要があります。

Vercel へのデプロイ

本アプリは Vercel にデプロイしています。

Vercel で GitHub リポジトリを連携させることで、GitHub にプッシュすると自動的にデプロイが行われるようになります。

TODO

  • API リクエスト失敗時に UI でユーザーにエラーを通知できるようにする
  • ユニットテストを追加する
    • Storybook

(追記) 動作確認用 API サーバー

RESAS-API は 2025 年 3 月 24 日に提供が終了しますが、動作確認を行いたい場合は population-tracker-jp-api を使用することができます。population-tracker-jp で使用するための都道府県データを SQLite に保存しています。Express で作成した API サーバーです。あくまで動作確認用の簡易的な API サーバーですが、API キーの確認など最低限の処理は行っています。

まとめ

ゼロから自分自身で Next.js アプリケーションを作成することで Next.js や React について改めて理解を深めることができました。 コードの可読性や保守性、セキュリティなど、プロジェクトの品質を高く保つためにはどうすればよいかを意識しながら開発を行うように努めました。

© 2024 seelx3