RESTful API を作成する!!(with Next.js & MongoDB & TypeScript)


はじめに

Next.jsの勢いが止まらない今、Next.js 第一弾として NoSQLのMongoDBと連携し、簡単なAPIを作成する手順をまとめました。

認識が甘い点など各所で見受けられると思います。そのような点がございましたら、ご教授いただけると幸いです


Next.js & MongoDB (TypeScript化) 簡易セットアップ

Next.js 公式の github 上にはたくさんの examples が掲載されています。

https://github.com/vercel/next.js/tree/master/examples
今回はこの中の with-typescript フォルダーをベースとします。

セットアップは非常に簡単で、ターミナルに移動し、以下のコマンドを叩くだけです。

​yarn create next-app --example with-typescript プロジェクト名

ファイル構成を確認します。

src フォルダーを作成し、components interfaces pages utils フォルダーを src 直下に配置します。

続いてMongoDBaxiosをインストールします。

yarn add mongodb axios
yarn add -D types/mongodb

本記事の目標

utils > sample-data から取得しているデータを、MongoDBを通して作り出したデータに置き換えることを再現したいと思います。


コードを書く

Next.js 公式の examples > with-mongodb を参考

①environment variables をセットアップ

ルートディレクトリに .env.local ファイルを作成します。

.env.local
MONGODB_URI= // Your MongoDB connection string
MONGODB_DB= // dbname

②mongodb.ts ファイル作成

utils フォルダー内、sample-data.ts ファイルを mongodb.ts にリネームし、以下のようにコードを書き換えます。

mongodb.ts
import { MongoClient, Db } from 'mongodb';

const { MONGODB_URI: uri, MONGODB_DB: dbName } = process.env;

let cachedClient: MongoClient;
let cachedDb: Db;

if (!uri) {
  throw new Error(
    'Please define the MONGODB_URI environment variable inside .env.local'
  );
}

if (!dbName) {
  throw new Error(
    'Please define the MONGODB_DB environment variable inside .env.local'
  );
}

export async function connectToDatabase() {
  if (cachedClient && cachedDb) { //キャッシュ変数が入力されているか確認
    return { client: cachedClient, db: cachedDb };
  }

  const client = await MongoClient.connect(uri!, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  const db = await client.db(dbName);

  cachedClient = client;
  cachedDb = db;

  return { client, db };
}

③ MongoDB でデータを作成

MongoDB内、Clusters > COLLECTIONS でデータベースを作成します。

次に INSERT DOCUMENT ボタンを押して、データを作成します。

複数個、データを作成しておきます。

エディタに移動し、取得するデータの型付けをします。(User に変更を加えます。)

src/interfaces/index.ts
export type User = {
-  id: number
+  _id:string
+  id:string
  name: string
}

④MongoDB との接続

MongoDB との接続を役割として持つ src > pages > api > users > index.ts に移動しコードを以下のように変更します。

api/users/index.ts
import { NextApiRequest, NextApiResponse } from "next";
import { connectToDatabase } from "../../../utils/mongodb";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const { method } = req;
    switch (method) {
      case "GET":
        // Get data from mongodb
        const { db } = await connectToDatabase();
        const data = await db.collection("user").find().toArray(); //連想配列化

        res.status(200).json(data); // json 形式でデータを取得

        break;
      default:
        res.setHeader("Allow", ["GET", "PUT"]);
        res.status(405).end(`Method ${method} Not Allowed`);
    }
  } catch (err) {
    res.status(500).json({ statusCode: 500, message: err.message });
  }
};

export default handler;

上述のコードは、next.js > examples > api-routes-rest >pages >api >user/[id].js を参考にしました。↓

https://github.com/vercel/next.js/blob/canary/examples/api-routes-rest/pages/api/user/[id].js

/api/user にアクセスするとデータが取得できていることが確認できます。
http://localhost:3000/api/users

各コンポーネントにも変更を加えていきます。

users/index.ts

pages/users/index.ts
-import { GetStaticProps } from 'next'
+import { GetServerSideProps } from "next";
import Link from 'next/link'

import { User } from '../../interfaces'
import { sampleUserData } from '../../utils/sample-data'
import Layout from '../../components/Layout'
import List from '../../components/List'

type Props = {
  items: User[]
}

const WithStaticProps = ({ items }: Props) => (
  <Layout title="Users List | Next.js + TypeScript Example">
    <h1>Users List</h1>
    <p>
      Example fetching data from inside <code>getStaticProps()</code>.
    </p>
    <p>You are currently on: /users</p>
    <List items={items} />
    <p>
      <Link href="/">
        <a>Go home</a>
      </Link>
    </p>
  </Layout>
)

-export const getStaticProps: GetStaticProps = async () => {
-  // Example for including static props in a Next.js function component --page.
-  // Don't forget to include the respective types for any props passed -into
-  // the component.
-  const items: User[] = sampleUserData
-  return { props: { items } }
-}

+export const getServerSideProps: GetServerSideProps = async () => {
+  const response = await axios.get<User[]>+("http://localhost:3000/api/users");
+  const items = await response.data;
+
+  return { props: { items } };
+};

export default WithStaticProps

SSG(getStaticProps関数を使用すると)ではビルド時のみのデータ取得を行うため、頻繁に変化のあるデータには向きません。
そこで SSR(getServerSideProps関数を使用)化することで、ビルド時に実行され、またリクエストごとにデータ取得を行います。

getServerSideProps の関数内部では、外部データを取得して、取得したデータを props としてページに渡すことができます。
コードでも確認できるように、items prop は WithStaticProps コンポーネントに渡されます。

users/[id].tsx

pages/users/[id].tsx
import { GetStaticProps, GetStaticPaths } from "next";

import { User } from "../../interfaces";
import Layout from "../../components/Layout";
import ListDetail from "../../components/ListDetail";
import axios from "axios";

type Props = {
  item?: User;
  errors?: string;
};

const StaticPropsDetail = ({ item, errors }: Props) => {
  if (errors) {
    return (
      <Layout title='Error | Next.js + TypeScript Example'>
        <p>
          <span style={{ color: "red" }}>Error:</span> {errors}
        </p>
      </Layout>
    );
  }

  return (
    <Layout
      title={`${
        item ? item.name : "User Detail"
      } | Next.js + TypeScript Example`}
    >
      {item && <ListDetail item={item} />}
    </Layout>
  );
};

export default StaticPropsDetail;

export const getStaticPaths: GetStaticPaths = async () => {
  // Get the paths we want to pre-render based on users
  const response = await axios.get<User[]>("http://localhost:3000/api/users");
  const items = await response.data;

  const paths = items.map((user) => ({
    params: { id: user.id },
  }));

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false };
};

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export const getStaticProps: GetStaticProps = async ({ params }) => {
  try {
    const id = params?.id;
    const response = await axios.get<User[]>("http://localhost:3000/api/users");
    const items = await response.data;

    const item = items.find((data) => data.id === id);
    // By returning { props: item }, the StaticPropsDetail component
    // will receive `item` as a prop at build time
    return { props: { item } };
  } catch (err) {
    return { props: { errors: err.message } };
  }
};

角括弧で名前を持つファイルは、Next.js では動的なページになります。
そして、getStaticPaths という非同期関数を使って、動的ルーティングによりページを静的に生成することを可能にします。この関数の中では、id として とりうる値 のリストを返さなければなりません。

最後に getStaticProps を実装します。今回のケースでは受け取った id に基づいて必要なデータを取得します。getStaticProps は params を受け取りますが、そこには id が含まれています。

以上の変更により、正常にデータが反映されています。

ユーザ情報に URL が対応付いており、RESTな API を作成できました!

以上になります。ここまで読んでいただきありがとうございました!!