Next.js と TypeScript で、REST API を学ぶ

TwitterFacebookHatena

TL;DR

このページでは、REST API の作り方について解説しますね。Next.js と TypeScript を駆使して REST API を作成し、それがどう動いて、何をやってくれるのか一緒に学びましょう。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

REST API とは?

あなたが小さな冒険者で、何か情報を探していると想像してみてください。例えば、あなたが図書館に本を探しに行ったとしましょう。そこで図書館員さんに「この本がどこにあるか教えてください」と尋ねたら、図書館員さんが教えてくれるかと思います。

これが REST API の一番基本的な考え方です。あなたが図書館員さん(REST API)に問い合わせ(リクエスト)をし、図書館員さんが答え(レスポンス)を返してくれます。

同じ考え方で、REST API はウェブ上で情報を取得したり、情報を変更したり、新しい情報を作成したり、情報を削除したりするための一連のルールや手段です。これを用いることで、ウェブページやアプリケーションは情報を交換できます。以下は、REST API を用いて情報を取得する基本的なコード例です。

// ファイル名: pages/api/books.ts
import type { NextApiRequest, NextApiResponse } from 'next'

const handler = (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: 'Next.js', author: 'Vercel' })
}

export default handler

このコードは、本の名前と著者を取得する簡単な API を作成します。あなたが図書館員さんに尋ねるように、他のコードがこの API にリクエストを送り、そしてこの API はレスポンスとして本の名前と著者を返します。

関数名・引数名は任意

handler という名前は単に一例なので、任意の名前に変更することができます。Next.js は関数名ではなく、エクスポートされた関数自体を API エンドポイントのハンドラーとして利用します。

以下に、ハンドラー関数の名前を helloHandler に変更した例を示します。

// src/pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function helloHandler(req: NextApiRequest, res: NextApiResponse<Data>) {
  res.status(200).json({ name: 'John Doe' })
}

ちなみに、res や req という名前も固定ではありません。これらは慣習的によく使われる名前ですが、開発者が任意の名前に変更することができます。

このように関数名を変更しても、API エンドポイントの動作に影響はありません。ただし、ソースコードを他人と共有する場合や、自分自身が後日見返す際に、関数の目的が一目でわかるように、意味のある名前をつけることが推奨されます。

EC サイトを作ることができる

実は、この REST API を使ったデータ取得の仕組みは、EC サイトやブログ、ニュースサイトなど、インターネット上の様々なサイトで活用されています。

例えば、EC サイトでは商品のリストや詳細、ユーザーの購入履歴などのデータをサーバーから取得して表示します。これらのデータは、REST API を通じて取得され、ユーザーがブラウザ上で見られる形に加工されます。

また、ブログやニュースサイトでは記事の一覧や詳細などのデータをサーバーから取得します。これらの情報もまた、REST API を使って取得され、読みやすい形に整形されます。

このように、REST API はウェブ上の様々な場所で使われ、データの取得や送信に大いに役立っています。ですから、REST API の仕組みを理解することは、ウェブ開発スキルを高めるために重要なステップとなります。

さて今度は、デフォルトで作成される Next.js の API ファイルを見てみましょう。

// src/pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

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

このコードは、Next.js を用いた API エンドポイントの基本的な構造を示しています。ここでは、handlerという名前の関数が API のハンドラーとしてエクスポートされています。このハンドラーは、req(リクエスト)とres(レスポンス)の 2 つの引数を取ります。これらは Express.js などの他の Node.js ベースのフレームワークと同じように機能します。

Next.js により提供されるNextApiRequestNextApiResponse型を使用して、リクエストとレスポンスオブジェクトの型を定義します。この型付けにより、TypeScript はリクエストとレスポンスオブジェクトが持つべきプロパティとメソッドについての情報を持つことができ、これにより開発者は安全にこれらのオブジェクトを利用することができます。

type Data = { name: string }はレスポンスの形式を定義しています。この例では、レスポンスの形式はnameというキーを持つオブジェクトで、その値は文字列でなければなりません。

handler関数の中では、まずレスポンスの HTTP ステータスコードを 200(成功)に設定します。そして、JSON 形式でレスポンスのボディを送信します。このボディは{ name: 'John Doe' }という形式のオブジェクトで、これは上で定義したData型と一致します。

API の取得・出力方法

作成した API は、Next.js のサーバーサイドルーティングにより、そのファイルパスに基づく URL からアクセスできます。この例では、ファイルがpages/api/hello.tsに保存されていると仮定すると、この API はhttp://localhost:3000/api/hello(またはあなたのデプロイした URL に応じて)から GET リクエストを行うことでアクセスできます。リクエストを行うと、レスポンスとして{ name: 'John Doe' }の形式の JSON が返されます。

hello.tsが存在する API エンドポイントは、Next.js アプリケーション内の任意の場所から fetchaxios などの HTTP リクエストライブラリを使って取得することができます。API エンドポイントは、ファイルが配置されているパスに基づいて URL が決定されます。この例では、hello.tssrc/pages/apiディレクトリ内に存在しているため、その URL は /api/hello となります。

例えば、src/pages/index.tsx ファイル内でこの API エンドポイントを呼び出すことができます。以下にそのサンプルコードを記載します。

// src/pages/index.tsx
import { useEffect, useState } from 'react'

type Data = {
  name: string
}

const IndexPage = () => {
  const [name, setName] = useState<string>('')

  useEffect(() => {
    const fetchData = async () => {
      const res = await fetch('/api/hello')
      const data: Data = await res.json()
      setName(data.name)
    }
    fetchData()
  }, [])

  return <div>Hello, {name}!</div>
}

export default IndexPage

上記のサンプルコードでは、React のuseEffectフックとuseStateフックを利用しています。useStateフックを使って name の状態を管理し、useEffectフック内で非同期的に API を呼び出し、取得したデータで name の状態を更新しています。

fetch('/api/hello') で API を呼び出し、res.json()で取得したデータを JSON 形式にパースし、そのデータを name の状態にセットしています。最終的には、<div>Hello, {name}!</div> により取得した名前がブラウザ上に表示されます。

エンドポイントの作成方法

エンドポイントとは、ネットワークに接続されたデバイスやアプリケーションが通信を行うための「目的地」のようなものです。具体的には、API でデータをやり取りするための URL の一部を指します。

たとえば、「https://xxxx.com/api/users」があったとき、「/api/users」部分がエンドポイントとなります。このエンドポイントに対して特定の HTTP メソッド(GET, POST など)を使ってリクエストを送ることで、データの取得や更新ができます。

Next.js では、pages/api/ ディレクトリ内に作成されたファイルは自動的に API エンドポイントとして扱われます。そのため、各エンドポイントはそのファイルのパスに応じた URL でアクセスできます。

そして、フロントエンドからその API エンドポイントにアクセスする際には、 fetch 関数や axios のような HTTP クライアントライブラリを使って HTTP リクエストを行います。

たとえば、先ほどの pages/api/hello.ts をフロントエンドから呼び出すには、次のようなコードを書きます。

// src/pages/index.tsx
import { useEffect } from 'react'

const HomePage = () => {
  useEffect(() => {
    fetch('/api/hello')
      .then((response) => response.json())
      .then((data) => console.log(data))
  }, [])

  return <h1>Welcome to HomePage</h1>
}

export default HomePage

上記のコードは、HomePage コンポーネントがレンダリングされるときに /api/hello への HTTP リクエストを非同期で行い、その結果をコンソールに出力します。これにより、サーバーサイドの API とフロントエンドのコンポーネント間でデータをやり取りすることができます。

そして、.then というメソッドを使って、非同期処理の結果を取得しています。

.then とは?

.then は、JavaScript で非同期処理を扱う際によく見る表現で、プロミス(Promise)という仕組みに関連しています。

プロミスは、「未来のある時点で結果を返す」ことを約束する仕組みで、例えばネットワークを通じて情報を取得するときに使われます。ネットワークを通じてデータを取得する際は、リクエストを送信してからデータが返ってくるまでに時間がかかるため、その間に他の処理を行うことができます。そのような処理を「非同期処理」といいます。

.then を使うと、プロミスが「完了したら、次に何をすべきか」を記述することができます。つまり、.then の後ろに書かれた関数は、プロミスが完了した後に実行されます。

例えば、先ほどのコードを見てみましょう。

fetch('/api/hello')
  .then((response) => response.json())
  .then((data) => console.log(data))

このコードでは、まず fetch('/api/hello') でデータを取得するプロミスが作られます。そして、そのプロミスが完了したら(つまり、データが取得できたら)、.then(response => response.json()) の部分が実行されます。この部分では、取得したデータ(response)を JSON 形式に変換しています。

その次に、再び .then(data => console.log(data)) の部分が実行されます。ここでは、先ほど JSON 形式に変換したデータ(data)をコンソールに出力しています。

要するに、.then を使うと、「この処理が終わったら、次にこれをやってね」という指示を書くことができるのです。

Next.js で、REST API を実装

では、より具体的な例として、Next.js を使って本の情報を管理する API を作ってみましょう。

// ファイル名: pages/api/books.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Book = {
  id: string
  name: string
  author: string
}

let books: Book[] = [
  { id: '1', name: 'Harry Potter', author: 'J.K. Rowling' },
  { id: '2', name: 'The Lord of the Rings', author: 'J.R.R. Tolkien' },
]

const handler = (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'GET') {
    res.status(200).json(books)
  }
}

export default handler

このコードは、本のリストを返す API を作成します。まず Book という型を定義し、それに基づいて books という本の配列を作ります。そして、リクエストのメソッドが GET の場合には、その books 配列をレスポンスとして返します。これにより、他のコードがこの API に GET リクエストを送ると、本のリストが返されます。

REST API を実装

では、上記のコードをさらに拡張して、新たな本を追加する機能を実装してみましょう。そのためには、POST リクエストを処理する必要があります。

// ファイル名: pages/api/books.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Book = {
  id: string
  name: string
  author: string
}

let books: Book[] = [
  { id: '1', name: 'Harry Potter', author: 'J.K. Rowling' },
  { id: '2', name: 'The Lord of the Rings', author: 'J.R.R. Tolkien' },
]

const handler = (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'GET') {
    res.status(200).json(books)
  } else if (req.method === 'POST') {
    const newBook: Book = req.body
    books.push(newBook)
    res.status(200).json(newBook)
  }
}

export default handler

このコードでは、POST リクエストを受け取った際に、リクエストの本文に含まれる新たな本のデータを books 配列に追加します。その後、新たに追加した本のデータをレスポンスとして返します。これにより、他のコードがこの API に POST リクエストを送ると、新たな本が追加され、その新たな本のデータが返されます。

Emotion で実装

Emotion は CSS-in-JS の一つで、コンポーネントのスタイルを JavaScript の中に書くことができます。ここでは、上記の REST API を用いて、本のリストを表示するコンポーネントのスタイルを Emotion を使って書いてみましょう。

// ファイル名: components/BookList.tsx
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

type Book = {
  id: string
  name: string
  author: string
}

type Props = {
  books: Book[]
}

const BookList = ({ books }: Props) => {
  return (
    <ul css={styles.list}>
      {books.map((book) => (
        <li key={book.id} css={styles.item}>
          <h2 css={styles.title}>{book.name}</h2>
          <p css={styles.author}>{book.author}</p>
        </li>
      ))}
    </ul>
  )
}

const styles = {
  list: css`
    list-style: none;
    padding: 0;
  `,
  item: css`
    margin-bottom: 1rem;
  `,
  title: css`
    font-size: 1.2rem;
    font-weight: bold;
  `,
  author: css`
    font-size: 1rem;
    color: #888;
  `,
}

export default BookList

ここでは、本のリストを受け取り、それをリスト形式で表示する BookList コンポーネントを作成します。そして、そのコンポーネントのスタイルを Emotion を使って定義します。リスト、リストの各アイテム、本の名前、そして著者の名前それぞれにスタイルを適用しています。このように、Emotion を使うと、コンポーネントのスタイルを直接 JavaScript の中に書くことができ、スタイルとコンポーネントのロジックを密接に結びつけることができます。

Next.js と TypeScript で、REST API を学ぶ