Next.jsはその優れたサーバーサイドレンダリング機能と静的サイト生成機能で広く知られていますが、それだけではありません。
Next.jsは強力なAPI開発機能も提供しており、フルスタックのWebアプリケーション開発をシームレスに行うことができます。
この記事では、Next.jsのルートハンドラーを活用してAPIを開発する方法について詳しく解説します。
GETリクエストとPOSTリクエストの基本的なハンドリング方法を中心に、簡単なサンプルコードを交えながらステップバイステップで説明します。
GETリクエストを受け取ってデータを返す方法、POSTリクエストでデータを受け取り処理する方法を実装していきます。
Next.jsを使ったAPI開発の基本をマスターすることで、フロントエンドとバックエンドの統合がさらにスムーズになり開発効率が飛躍的に向上します。
Next.js14におけるルートハンドラーのルール
基本的にはpage.tsxのルーティングと同じくディレクトリ構成がURLのパス構成につながります。
またファイル名はroute.tsとします。
例えば以下のような構成にしてみます。
- src
|-app
|-hello
|-route.ts
export async function GET() {
return new Response('Hello world');
}
localhost:3000がトップページにあたるのでlocalhost:3000/helloとするとroute.tsで書いていたレスポンスの内容がブラウザに表示されています。
また入れ子のディレクトリにするとURLのパスも「/」で区切ったことになります。
例えば以下のような以下のような構成にします。
- src
|-app
|-dashboard
|-users
| |-route.ts
|-route.ts
export async function GET() {
return new Response('dashboard');
}
export async function GET() {
return new Response('user');
}
dashboardというディレクトリ自体にはlocalhost:3000/dashboardでアクセスできて、usersというディレクトリにはlocalhost:3000/dashboard/usersでアクセスできます。
1点だけ注意としてはpage.tsxとroute.tsは同じ階層に置くとページが正しく表示されないことがあります。
- src
|-app
|-profile
|-route.ts
|-page.tsx
page.tsxは画面表示を担当するのですがroute.tsと同階層にあることで、API担当のroute.tsの方をブラウザに移そうとするからです。
そのため以下のようにしてroute.tsはpage.tsxとは同じ階層にならないように分けておく必要があります。
- src
|-app
|-profile
|-api
| |-route.ts
|-page.tsx
Next.js14のルートハンドラーでAPI開発をする
ルートハンドラーを使ったAPI開発ではHTTPメソッドが使えます。
またHTTPメソッドはgetリクエストならGET( )、postリクエストならPOST( )といった具合に決められたメソッドを実行することで実装できるようになります。
以下のようなディレクトリ構成があったとします。
- src
|-app
|-comments
|-data.ts
|-route.ts
データベースは使わずにdata.tsというファイルに手動でデータを置いておきたいと思います。
export const comments = [
{
id: 1,
text: 'コメント1',
},
{
id: 2,
text: 'コメント2',
},
{
id: 3,
text: 'コメント3',
},
];
data.tsの中のデータを取得しようとしたら例えばroute.tsでgetリクエストは以下のように書きます。
import { comments } from './data';
export async function GET() {
return Response.json(comments);
}
VSCODEを使っている場合にはThunder Clientというプラグインが便利です。
インストールして開くと「New Request」というボタンがあり、作成したAPIが実行できる環境を作れます。
今回はcommentsというディレクトリなのでlocalhost:3000/commentsがURLになります。
またURLの左にあるプルダウンでリクエスト名を選択できますがデフォルトではGETになっていますので今回は何もしなくて大丈夫です。
リクエスト名とURLを入力したら、「Send」ボタンをクリックすると実行結果が返ってきます。
またpostリクエストは以下のように書きます。
import { comments } from './data';
export async function POST(request: Request) {
const comment = await request.json();
const newComment = {
id: comments.length + 1,
text: comment.text,
};
comments.push(newComment);
return new Response(JSON.stringify(newComment), {
headers: {
'Content-Type': 'application/json',
},
status: 201,
});
}
Thunder Clientから「New Request」でURLは引き続きlocalhost:3000/commentsですが、URLの左にあるリクエスト名は「POST」を選択しておきます。
またURLの下の「Body」タブをクリックすると送信内容を入力できますのでJSON形式で入力してから、先ほどと同じく「Send」ボタンをクリックして実行します。
正しく新しいデータが追加されているか、再度GETリクエストに切り替えて実行してみると確認できます。
また個別のデータのみを指定して取得する方法を紹介します。
例えばcommentsの中からidを指定して特定のデータのみを指定するケースです。
そのような場合には以下のようにダイナミックルートでディレクトリを切った状態にします。
- src
|-app
|-comments
|-[id]
| |-route.ts
|-data.ts
|-route.ts
import { comments } from '../data';
export async function GET(
_request: Request,
{ params }: { params: { id: string } }
) {
const comment = comments.find(
(comment) => comment.id === parseInt(params.id)
);
return Response.json(comment);
}
paramsで取得できるURLの一部であるidと配列commentsの中のidプロパティが一致するものをfindメソッドで探しています。
Thunder Clientでは以下のようにURLを指定して実行してみます。
上図では配列commentsの中からidプロパティが「1」のものを取得するためのエンドポイントになっています。
「Send」ボタンを実行すると以下のように取得できています。
このように個別のデータを指定して取得できるようになるとデータの「修正」「削除」ができるようになります。
例えば修正についてはPATCHメソッドというものを使って以下のように書きます。
import { comments } from '../data';
export async function PATCH(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const { text } = body;
const index = comments.findIndex(
(comment) => comment.id === parseInt(params.id)
);
comments[index].text = text;
return Response.json(comments[index]);
}
paramsで取得できるURLの一部であるidと配列commentsのidプロパティの値が一致するものを探して、該当のデータが配列commentsの中でどのインデックス番号になっているかをfindIndexメソッドを使って取得します。
comments[index]といった形で指定したidが含まれるデータが取得できるようになるので、comments[index]のtextプロパティの値をrequest.json()で送る修正内容のtextに再代入する形でデータの修正を行っています。
Thunder Clientではメソッドを「PATCH」にして先ほどと同じURLにします。
またURLの下の「Body」タブをクリックすると修正内容を入力できますのでJSON形式で入力してから、先ほどと同じく「Send」ボタンをクリックして実行します。
実行するとtextプロパティがBodyタブで入力していたJSON形式の中身のものに変わっていることがわかります。
正しく新しいデータが修正されているか、再度GETリクエストに切り替えて実行してみると確認できます。
GETメソッドを使ってhttp:localhost:3000としたときの一覧表示でも修正されていることがわかります。
この方法を応用して特定のデータの削除もできます。
データの削除はDELETEメソッドというものを使って以下のように書きます。
import { comments } from '../data';
export async function DELETE(
_request: Request,
{ params }: { params: { id: string } }
) {
const index = comments.findIndex(
(comment) => comment.id === parseInt(params.id)
);
console.log(index);
const deletedComment = comments[index];
comments.splice(index, 1);
return Response.json(deletedComment);
}
findIndexを使ってURLで指定したidが含まれるデータを配列commentsの中から特定するところまでは同じです。
削除自体はspliceメソッドを使います。
spliceメソッドは配列に使えるメソッドで第一引数に削除したいインデックス番号、第二引数にそのインデックス番号から始まって何個分のデータを削除するかを指定します。
今回はURLで指定したidが含まれるデータ1個のみを削除したいのでsplice(index, 1)としました。
Thunder ClientではURLでidを指定しなからメソッドを「DELETE」としておき実行します。
正しく新しいデータが削除されているか、再度GETリクエストに切り替えて実行してみると確認できます。
GETメソッドを使ってhttp:localhost:3000としたときの一覧表示でもidプロパティが「1」のデータが削除されていることがわかります。
Next.js14のルートハンドラーでクエリ検索を実装する
コメント情報の取得の中でクエリ検索を実装してみたいと思います。
コメント情報の取得は/src/app/components/route.tsのGETメソッドで実装しています。
import { comments } from './data';
export async function GET() {
return Response.json(comments);
}
上記のコードをもとに以下のように書き換えます。
import { NextRequest } from 'next/server';
import { comments } from './data';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('query');
const filtered = query
? comments.filter((comment) => comment.text.includes(query))
: comments;
return Response.json(filtered);
}
GETメソッドの引数にrequestをとっておき型名をNextRequestというものを指定しておきます。
これによってnextUrl.searchParamsというプロパティにアクセスすることができて、URLのパラメーター群を取得することができます。
例えば「http://localhost:3000/comments?query=コメント1」のような検索をした時に「?」以降の部分の情報のことです。
定数searchParamsをコンソールで確認してみると以下のようになります。
上図のようにJSON形式でURLのパラメーター群を取得できていますね。
今回の場合だとqueryというキーに対して検索キーワードを入れているのでsearchParams.get(“query”)とすることで検索キーワード自体を取得できるわけです。
あとはfilterメソッドを使ってデータの中に該当するものがあるかを検索して返してあげるわけです。
ブラウザで「http://localhost:3000/comments?query=コメント1」とすると「コメント1」という検索キーワードにヒットしたデータだけが表示されるようになります。
Next.js14のルートハンドラーでリダイレクト処理を作る
続いてルートハンドラーでリダイレクト処理を作ってみます。
もともとは[id]のような名前のフォルダを作って個別のID名をURLで指定すれば対象の個別データを取得できるようにしていました。
- src
|-app
|-comments
|-[id]
| |-route.ts
|-data.ts
|-route.ts
import { comments } from '../data';
export async function GET(
_request: Request,
{ params }: { params: { id: string } }
) {
const comment = comments.find(
(comment) => comment.id === parseInt(params.id)
);
return Response.json(comment);
}
上記のコードにおいて「存在しないIDを指定された時に一覧ページにリダイレクトする」ような記述を追加していきます。
現状だとコメントはid:1、id:2、id:3という3つのデータのみですが、「http://localhost:3000/comments/4」
のような存在しないリクエストを送るとエラーになるようになっているからです。
リダイレクト処理はNext.jsではredirectという専用のメソッドがあり以下のように書くことができます。
import { comments } from '../data';
export async function GET(
_request: Request,
{ params }: { params: { id: string } }
) {
// ここを追加
if (parseInt(params.id) > comments.length || parseInt(params.id) === 0) {
redirect('/comments');
}
const comment = comments.find(
(comment) => comment.id === parseInt(params.id)
);
return Response.json(comment);
}
redirectメソッドは引数にリダイレクト先をパスで指定することで使用できます。
今回はコメントの一覧ページにリダイレクトさせたいので「”/comments”」にしています。
条件の判定には「データの個数より大きい数字を指定したとき」もしくは「0を指定したとき」が存在しないURLということでリダイレクトさせました。
Next.js14のルートハンドラーでヘッダー情報を操作する方法
続いてはヘッダー情報の取得や追加をしてみます。
まずヘッダー情報の取得ですがNext.jsではheadersというメソッドがあります。
以下のprofileフォルダの中で書いてみます。
- src
|-app
|-profile
|-api
| |-route.ts
|-page.tsx
export async function GET() {
const requestHeaders = headers();
console.log(requestHeaders.get('User-Agent'));
}
この状態で「http://localhost:3000/profile/api」にアクセスすると以下のようにgetメソッドで指定したヘッダー情報を取得することができます。
headersメソッドの中にはヘッダー情報の各種が入っており、getメソッドの引数で取得したいプロパティ名を指定することで取得できるようになっているためです。
またヘッダー情報を追加することも可能です。
例えば現状だと以下のようにHTML形式でレスポンスを書いてもHTMLタグはそのまま画面上に表示されてしまいます。
export async function GET() {
return new Response('<h1>profile api</h1>');
}
これはデフォルトのヘッダー情報ではHTML形式でデータを表示するようになっていないからです。
HTML形式に変換できるようにヘッダー情報として「Content-Type:text/html」を追加してみます。
export async function GET() {
// ここを変更
return new Response('<h1>profile api</h1>', {
headers: {
'Content-Type': 'text/html',
},
});
}
再度「http://localhost:3000/profile/api」にアクセスするとHTMLとして変換されていることがわかります。
Next.js14のルートハンドラーでクッキーを操作する
続いてはクッキーの作成と取得をやってみます。
Next.jsではcookiesというメソッドがありますので以下のように書きます。
export async function GET() {
// クッキーの作成
cookies().set('page', '10');
// クッキーの取得
console.log(cookies().get('page'));
return new Response('<h1>profile api</h1>', {
headers: {
'Content-Type': 'text/html',
},
});
}
setメソッドで第一引数にキーを第二引数に値を書くことでクッキー情報を新規作成できます。
また作成したクッキーはgetメソッドの引数にキー名を指定することで取得できるようになります。
Next.js14のルートハンドラーでキャッシュを無効化する
続いてはキャッシュについての設定です。
Next.jsではデフォルトでキャッシュが有効になっていますので、何もしなくてもキャッシュされたページが表示されるようになります。
ちなみに「npm run dev」コマンドで実行する開発環境ではキャッシュは無効になっていますので注意してください。
開発環境ではキャッシュが無い前提で作っていたのに本番環境ではデフォルトのキャッシュが効いて不都合なケースがあるからです。
デフォルトのキャッシュを無効化するには以下のようなコードを書き足します。
// キャッシュを無効化する
export const dynamic = 'force-dynamic';
export async function GET() {
cookies().set('page', '10');
console.log(cookies().get('page'));
return new Response('<h1>profile api</h1>', {
headers: {
'Content-Type': 'text/html',
},
});
}
キャッシュの有無については作りたいアプリケーションの機能から考えて適切に設定するようにしましょう。
Next.js14のルートハンドラーでミドルウェアを実装する
最後にミドルウェアの実装のやり方を紹介します。
Next.jsでは以下のようにsrcフォルダ直下に「middleware.ts」というファイルを作成することでミドルウェアを作ることができます。
- src
|-app
| |-...
| |-...
| |-...
|-middleware.ts
ファイルの中身ですが、例えば特定のURLに移動するとトップページにリダイレクトするミドルウェアを作る場合は以下のように書きます。
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// profileにアクセスしたらトップページにリダイレクト
if (request.nextUrl.pathname === '/profile') {
return NextResponse.redirect(new URL('/', request.url));
}
}
// profileページでミドルウェアを設定
export const config = {
matcher: '/profile',
};
関数middlewareの中身は「/profile」というURLに行こうとすると自動的にトップページにリダイレクトされるようになっています。
またミドルウェアの場合、どこの階層で作ったミドルウェアを有効化するかを設定することになります。
定数configではmatcherというキーをとって、値にはミドルウェアを有効化したいパスを指定することで特定のページでのみ作ったミドルウェアを有効化することになります。