Next.jsはフロントエンドフレームワークとしてAPIにアクセスを行い情報を取得するだけではなくフロントエンドからアクセス可能なAPIを作成することができます。つまり同じコードベースの中でフロントエンド側のコードとバックエンド側のコードを記述できることを意味します。Node.js上にExpressサーバなどを利用して別にサーバを立てなくてもAPI Routesを経由してデータベースにアクセスすることも可能です。API Routesを利用するために特別な設定は必要ないのでプロジェクト作成直後から利用することができます。

本文書ではNext.jsのドキュメントに掲載されているAPI Routesの例ではどのようなことができるのかよくわからないという人を対象に説明を行っています。API Routesを利用したRequest、Responseの簡単な動作確認とAPI Routesを経由したデータベースからのデータ取得の方法、GraphQLサーバの設定を確認してAPI Routesの理解を深めていきます。

API RoutesはServerless Functions

API Routesに記述したコードはserverless functions(サーバレス)としてデプロイされます。サーバレスという名前からサーバがないというイメージを持つかもしれませんがserverless functionsを利用するとサーバを管理する必要がなくなりserverless functionsを提供するクラウドプロバイダーがサーバを管理してくれます。そのため開発者はserverless functionsに記述するコードの作成のみに集中することができます。またserverless functionsは外部からリクエストがあった場合のみ起動を行い実行して停止するというサイクルを持っているためサーバを常時起動しておく必要はなくリエクストがある回数のみ起動/実行/停止を繰り返すことになります。

API Routesとserverless functionsの関係についてはドキュメントのDeploymentに”API Routes are automatically optimized as isolated Serverless Functions that can scale infinitely”と記載されています。
fukidashi

Next.jsプロジェクトの作成

API Routesの動作確認を行うためにNext.jsプロジェクトの作成を行います。npx create-next-appコマンドにプロジェクト名を指定してNext.jsのプロジェクトを作成します。ここではプロジェクト名にnext-js-api-routeという名前をつけていますが任意の名前をつけてください。


 % npx create-next-app next-js-api-route

コマンド実行するとnext-js-api-routeフォルダが作成されるので作成されたフォルダに移動してnpm run devコマンドを実行してください。


 % npm run dev

> next-js-api-route@0.1.0 dev
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 2.1s (124 modules)

http://localhost:3000にアクセスするとNext.jsの初期画面が表示されます。

next.jsのデフォルトページ
next.jsのデフォルトページ

API Routesの動作確認

プロジェクトのフォルダを見るとpagesフォルダの中にapiフォルダがありその下にhello.jsファイルが確認できます。


// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' })
}

API Routesでは作成した関数は必ずexportする必要があります。デフォルトで存在するhello.jsファイルの関数名はhandlerという名前がついていますが任意の名前をつけることができます。

handler関数では引数にreq(Requestの略), res(Responseの略)が入りjsonで”John Doe”をJSONで戻していることがわかります。statusメソッドでHTTPステータスコードの200を設定しています。

Next.jsではpagesフォルダの中にJavaScriptファイルを作成するとルーティング自動で設定されるようにAPI Routesではpages/apiフォルダの下にJavaScriptファイルを作成するとエンドポイントとしてルーティングが追加されます。

apiフォルダのhello.jsファイルはAPIエンドポイント/api/helloで外部からアクセスすることが可能となります。ブラウザからlocalhost:3000/api/helloにアクセスするとJSONデータとして戻されることが確認できます。

/api/helloへのアクセス
/api/helloへのアクセス

外部サービスへのアクセス

hello.jsファイルの例から登録されたAPIのエンドポイントにアクセスするとJSONが戻されることがわかりました。API Routesからさらに別にサービスのAPIにアクセスして情報を取得することもできます。

JSONPlaceHolderを利用して動作確認を行います。JSONPlaceHolderは無料のサービスで提供されるURLにアクセスするとJSONデータが戻されます。

/api/usersにアクセスするとユーザ一覧が表示できるようにapiフォルダの下にusersフォルダを作成しその下にindex.jsファイルを作成し以下のユーザ情報取得のコードを記述します。fetch関数でJSONPlaceHolderからユーザ一覧を取得してJSONデータとして戻しています。


export default async function handler(req, res) {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/')
  const users = await response.json()
  res.status(200).json({ users })
}

/api/usersにアクセスすると戻されるJSONデータを確認することができます。

/api/usersから戻されるJSONデータ
/api/usersから戻されるJSONデータ

Next.jsのフロントエンド側のコードからAPIの/api/usersにアクセスを行いユーザ一覧を表示できる確認を行います。pagesフォルダの直下にあるindex.jsに下記のコードを記述します。useEffectの中でfetch関数を利用して/api/usersにアクセスを行います。取得したデータはuseStateを利用してusers変数に保存してmap関数で展開しています。


import {useState,useEffect} from 'react'

export default function Home() {
  const [users, setUsers] = useState([])
  useEffect(() => {
    const fetchUsers = async () => {
      const response = await fetch('/api/users')
      const data = await response.json()
      setUsers(data.users)
    }
    fetchUsers()
  },[])

  return (
    <div>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

ブラウザからlocalhost:3000にアクセスするとユーザ一覧が確認できます。フロンエンド側からAPI Routesにアクセスを行い取得したデータをブラウザ上に表示できることがわかりました。

ユーザ一覧を表示
ユーザ一覧を表示

この例を見て外部のサービスにアクセスしたい場合は必ずAPI Routesを経由しないといけないのかと思う人もいるかもしれませんがあくまで例なのでAPI Routesを経由させる必要はありません。

サーバサイドレンダリングによるアクセス

先ほどの動作確認ではuseStateとuseEffectを利用してクライアントから/api/usersにアクセスを行いユーザ一覧を表示していました。Next.jsではクライアントからだけではなくgetServerSideProps関数を利用してサーバサイドから/api/usersにアクセスを行うことができます。getServerSideProps関数のfetch関数を利用する場合は引数のURLには相対パス(/api/users)ではなく絶対パス(http://localhost:3000/api/users)を設定する必要があります。


export async function getServerSideProps() {
  const response = await fetch('http://localhost:3000/api/users')
  const data = await response.json()

  return { props: { data } }
}

export default function Home({data}) {
  return (
    <div>
      <ul>
        {data.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

表示される内容はクライアントの場合と変わりませんがサーバ側でレンダリングが行われるため戻されるデータにはユーザ一覧の情報が含まれています。ページのソースを確認することでクライアントとサーバでのレンダリングの違いを確認することができます。

Dynamic API Routes

API RoutesもDynamic Routesに対応しているでusersフォルダの下に[id].jsファイルを作成して動作確認を行います。

Dynamic API Routesを設定すると/api/users/1, /api/users/2,…,/api/users/100など動的に変わるURLの値をreqオブジェクトに含まれるquery.idから取得することができます。idはファイル名に指定したブラケットの中のidに一致します。


export default function handler(req, res) {
  res.status(200).json({ id: req.query.id  })
}

ブラウザから/api/users/100にアクセスすると100は文字列として以下のようにJSONデータが戻されます。


{"id":"100"}

POSTリエクスト

ここまではAPI Routesに対してすべてGETリクエストを送信していました。API RoutesではPOSTリクエストによってデータを受け取ることもできます。リクエストがGETリクエストなのかPOSTリクエストなのかははreq.methodで確認することができます。

usersフォルダのindex.jsにconsole.logを追加しreq.methodの中身を確認します。


export default async function handler(req, res) {
  console.log(req.method)
  const response = await fetch('https://jsonplaceholder.typicode.com/users/')
  const users = await response.json()
  res.status(200).json({ users })
}

/api/usersにアクセスするとnpm run devコマンドを実行しているコンソールに”GET”が表示されます。API Routesの中の処理はサーバ側で実行されるためブラウザ側のコンソールには何も表示されません。

クライアントからPOSTリクエストを送信するためにpages/index.jsファイルを書き換えます。fetch関数ではjsonでデータを送信するためheadersでContent-Typeをapplication/jsonに設定しています。送信したいデータはbodyに設定します。


import { useEffect } from 'react';

export default function Home() {
  useEffect(() => {
    const postData = async () => {
      await fetch('/api/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name: 'John' }),
      });
    };
    postData();
  }, []);

  return (
    <div>
      <h1>ユーザ</h1>
    </div>
  );
}

ブラウザでhttp://localhost:3000にアクセスするとPOSTリクエストを送信しているのでコンソールにはGETではなくPOSTが表示されます。

POSTリクエストではJSONデータを送信しているのでAPI RoutersでJSONデータを取得します。データはreq.bodyの中に含まれています。


export default async function handler(req, res) {
  console.log(req.body);
  res.status(200).json({ name: 'John Doe' });
}

コンソールには送信されてきたデータ表示されます。


{ name: 'John' }

req.methodの中にはメソッド、req.bodyの中にはPOSTリクエストから送信されているデータが含まれ取得できることが確認できました。

POSTリエクストではなくGETリクエストのURLにパラメータをつける場合があります。


http://localhost:3000/api/hello?firstName=john&lastName=doe

URLにつけたパラメータについてはreq.queryの中に含まれます。

GET or POST

GETリクエスト、POSTリクエストだけではなくその他のリクエストもreq.methodを確認することでどのリクエストか判断しメソッドによって処理を変えることができます。


export default function handler(req, res) {
  const { method } = req;

  switch (method) {
    case 'GET':
      res.json({ message: 'GETリクエスト' });
      break;
    case 'POST':
      res.json({ message: 'POSTリクエスト' });
      break;
    case 'PATCH':
      res.json({ message: 'PATCHリクエスト' });
      break;
    default:
      res.json({ message: 'GET/POST/PATCHでもないリクエストです。' });
      break;
  }
}

APIキーのあるサービスの利用

JSONPlaceholderのサービスはだれでも利用することができますが通常は外部のサービスを利用する場合はAPIキーなどを取得して設定を行う必要があります。

アカウントの登録が必要ですが無料で利用できるOpenWeatherサービスのAPIを利用して動作確認を行います。アカウントの取得とAPIの取得については公開済みの下記の文書の”4.OpenWeatherのAPI”を参考にしてください。

APIキーを取得することができたらURLのappidにAPIキーを設定することでqパラメータで設定した東京の現在の天気の情報を取得することができます。


https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=YOUR_API_KEY&lang=ja

API Routesの設定

pages/apiフォルダの下にweather.jsファイルを作成します。APIのキーは環境変数として設定を行うためプロジェクトフォルダ直下に.env.localファイルを作成します。.env.localファイルにOPEN_WEATHER_API_KEYという名前の環境変数としてAPIキーを保存します。YOUR_API_KEYには各自がサービスから取得したAPIキーの値を設定します。


OPEN_WEATHER_API_KEY=YOUR_API_KEY
環境変数の設定を行ったら変更を反映されるせためにnpm run devコマンドを再実行する必要があります。
fukidashi

weather.jsからfetch関数を利用しますが.env.localファイルに追加した環境変数はprocess.env.OPEN_WEATHER_API_KEYでアクセスすることができます。


const weather = async (req, res) => {
  const response = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${process.env.OPEN_WEATHER_API_KEY}&lang=ja`
  );
  const data = await response.json();
  res.status(200).json(data);
};

export default weather;

pagesフォルダのindex.jsファイルでは/api/weatherにアクセスして天気の情報を取得してブラウザ上に表示します。useState, useEffectのHookを利用しています。


import { useState, useEffect } from 'react';

export default function Home() {
  const [weather, setWeather] = useState('');
  useEffect(() => {
    const fetchWeather = async () => {
      const response = await fetch('/api/weather');
      const data = await response.json();
      setWeather(data);
    };
    fetchWeather();
  }, []);

  return (
    <div>{weather.weather && <p>東京の天気:{weather.weather[0].main}</p>}</div>
  );
}

ブラウザで確認すると現在の東京の天気を表示することができます。

取得した東京の天気を表示
取得した東京の天気を表示

OpenWeatherから戻されるデータには場所の緯度と経度や気温が含まれています。その中からweatherプロパティの配列に入っているmainプロパティを表示しています。


{
  coord: { lon: 139.6917, lat: 35.6895 },
  weather: [ { id: 803, main: 'Clouds', description: '曇りがち', icon: '04d' } ],
  base: 'stations',
  main: {
    temp: 291.3,
    feels_like: 290.24,
    temp_min: 289.63,
    temp_max: 292.48,
    pressure: 1015,
    humidity: 41
  },
  visibility: 10000,
  wind: { speed: 3.6, deg: 140 },
  clouds: { all: 75 },
  dt: 1647503023,
  sys: {
    type: 2,
    id: 2038398,
    country: 'JP',
    sunrise: 1647463803,
    sunset: 1647506970
  },
  timezone: 32400,
  id: 1850144,
  name: '東京都',
  cod: 200
}

クライアントでの設定

API Routesを利用しなくてもクライアント側から直接OpenWeatherのサービスにアクセスして天気の情報を取得することができます。クライアント側でも環境変数を利用しますが.env.localファイルのOPEN_WEATHER_API_KEYを利用することができません。クライアント側で利用する際にはNEXT_JS_を先頭につける必要があります。NEXT_JS_をつけることでクライアント(ブラウザ)側で利用するのかサーバ側で利用するのかが明確になります。


NEXT_PUBLIC_OPEN_WEATHER_API_KEY=YOUR_API_KEY

fetch関数のURLを変更します。環境変数はprocess.env.NEXT_PUBLIC_OPEN_WEATHER_API_KEYでアクセスできます。


// const response = await fetch('/api/weather');
const response = await fetch(
  `https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${process.env.NEXT_PUBLIC_OPEN_WEATHER_API_KEY}&lang=ja`
);

API Routesを利用した場合と同じように東京の天気がブラウザ上に表示されます。

クライアント側ではAPI Routesでも天気の情報を取得することができましたが大きな違いが一つあります。クライアント側の場合はfetch関数がブラウザで実行されるためAPIのキーがダウンロードするJavaScriptファイルの中に入っているためだれでもみることができます。しかしAPI Routesの場合はサーバ側でfetch関数が実行されるためブラウザからAPIキーを確認することができません。

getServerSideProps関数での設定

getServerSideProps関数を利用してAPI Routesと同様にサーバ側でfetch関数を実行して天気の情報を取得することができます。その場合に利用する環境変数はサーバ側なので先頭にNEXT_JSを付与する必要はありません。この場合もサーバ側で実行されるためAPI_KEYをブラウザから確認することはできません。


export async function getServerSideProps() {
  const response = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=Tokyo&appid=${process.env.OPEN_WEATHER_API_KEY}&lang=ja`
  );
  const data = await response.json();

  return { props: { data } };
}

import { useState, useEffect } from 'react';

export default function Home({ data }) {
  return <div>{data.weather && <p>東京の天気:{data.weather[0].main}</p>}</div>;
}

データベースの作成

API Routesからデータベースに接続してデータが取得できるか動作確認を行います。データベースには簡易的に利用できるSQLiteを利用します。macOSの場合はデフォルトから利用することができます。

データベースの初期設定

usersテーブルを作成して1件データを挿入します。

usersテーブルの構成情報は下記の通りとなります。id, email, nameを持ちidはautoincrementを設定しているので自動でIDが割り振れます。emailはユニークキーの設定を行っています。emailもnameも列の型をtextに設定しています。


id INTEGER PRIMARY KEY AUTOINCREMENT,
email text NOT NULL UNIQUE,
name text NOT NULL

プロジェクトフォルダの直下でsqlite3コマンドを実行します。sqlite3コマンドの引数にはデータベースファイルを指定してください。SQLIteではデータベースをファイル単位で管理するためファイル名をdatabase.sqliteと設定しています。データベースに接続する際はこの名前を利用します。実行後にdatabase.sqliteファイルがsqlite3コマンドを実行したフォルダに作成されます。


 % sqlite3 database.sqlite

sqlite3コマンドを実行するとデータベースに接続することができ、コマンドラインでテーブルを作成することができます。下記のcreate table文をコピー&ペーストすることでusersテーブルを作成することができます。


create table users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email text NOT NULL UNIQUE,
  name text NOT NULL
);

テーブルを作成後にユーザデータを1件挿入します。挿入にはSQLのinsert文を利用します。


sqlite> insert into users(email,name) values('john@test.com','John Doe');

挿入が完了したらselect文を実行しデータがusersテーブルに保存されているか確認します。insert文で挿入した1件のユーザ情報を確認することができます。


sqlite> select * from users;
1|john@test.com|John Doe

データベースへの接続

API Routesから作成したSQLiteデータベースにアクセスできるか確認を行います。SQLiteデータベースに接続するためにsqlite3ライブラリをnpmコマンドでインストールします。


 % npm install sqlite3

¥pages¥api¥usersフォルダ下のindex.jsファイルでsqlite3を利用してSQLiteデータベースに接続しusersテーブルに保存されているユーザ情報を取得して戻します。


import sqlite3 from 'sqlite3';

const selectAll = (db, query) => {
  return new Promise((resolve, reject) => {
    db.all(query, (err, rows) => {
      if (err) return reject(err);
      return resolve(rows);
    });
  });
};

export default async function handler(req, res) {
  const db = new sqlite3.Database('./database.sqlite');
  const users = await selectAll(db, 'select * from users');
  db.close();

  res.status(200).json({ users });
}

pagesフォルダのindex.jsファイルでgetServerSideProps関数からfetch関数で/api/usersにアクセスしてユーザ情報を表示します。useEfffectとuseStateを利用してgetServerSideProps関数を利用したサーバ側からではなくクライアントからでも設定することができます。


export async function getServerSideProps() {
  const response = await fetch('http://localhost:3000/api/users');
  const data = await response.json();

  return { props: { data } };
}

export default function Home({ data }) {
  return (
    <div>
      <ul>
        {data.users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

API RoutesからSQLiteデータベースに接続して取得したユーザ情報をブラウザ上に表示することができました。

API Routesを利用して取得したデータを表示
API Routesを利用して取得したデータを表示

API Routesを利用しなくてもgetServerSideProps関数はサーバ側で実行されるのでgetServerSideProps関数の関数の中でSQLiteデータベースに接続しユーザ情報を取得してpropsで渡すこともできます。


import sqlite3 from 'sqlite3';

const selectAll = (db, query) => {
  return new Promise((resolve, reject) => {
    db.all(query, (err, rows) => {
      if (err) return reject(err);
      return resolve(rows);
    });
  });
};

export async function getServerSideProps() {
  const db = new sqlite3.Database('./database.sqlite');
  const data = await selectAll(db, 'select * from users');
  db.close();

  return { props: { data } };
}

export default function Home({ data }) {
  return (
    <div>
      <ul>
        {data.map((user) => (
          <li> key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

GraphQLサーバの設定

API Routesを利用してGraphQLサーバを利用することもできます。GraphQLサーバにはApolloサーバを利用します。apollo-server-microのインストールを行います。microはNode.jsのHttpサーバです。Expressサーバのコンパクト版だと考えてください。


 % npx install apollo-server-micro

インストールが完了したら/pages/apiフォルダにgraphql.jsファイルを作成して以下のコードを記述します。


import { ApolloServer, gql } from 'apollo-server-micro';

const typeDefs = gql`
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello World',
  },
};

const apolloServer = new ApolloServer({ typeDefs, resolvers });

const startServer = apolloServer.start();

export default async function handler(req, res) {
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader(
    'Access-Control-Allow-Origin',
    'https://studio.apollographql.com'
  );
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept'
  );
  if (req.method === 'OPTIONS') {
    res.end();
    return false;
  }

  await startServer;
  await apolloServer.createHandler({
    path: '/api/graphql',
  })(req, res);
}

export const config = {
  api: {
    bodyParser: false,
  },
};

設定後http://localhost:3000/api/graphqlにアクセスします。Apolloサーバのデフォルトのランディングページが表示されます。

Apolloサーバのデフォルトのランディングページ
Apolloサーバのデフォルトのランディングページ

Query your serverをクリックします。

設定したGraphQLサーバに対してQueryを実行することができるSandboxが表示されます。左側のQueryに設定を行なったhelloを確認することができるのでhelloクエリーを実行すると”Hello World!”が戻されることがわかります。

Sandbox上でhelloクエリーの実行
Sandbox上でhelloクエリーの実行

API Routesを利用してGraphQLサーバが動作することを確認することができました。

Next.jsのAPI Routesではどのようなことができるのか疑問に思っていた人も外部のリソースさらにデータベースへの接続できることもわかったので理解は深まったのではないでしょうか。

Next.jsのAPI RoutesにGraphQLサーバを起動する下記の記事も公開しました。