前回の記事では、APIの基礎や使用される環境など基礎的な部分を紹介しました!(よろしければご覧ください!)
今回は実践編!やってみましょうー!
Next.jsとVercelとは
Next.jsとは
Next.jsとはReactをもとに作成されたフレームワークです
Reactを知っている人には親しみやすく、初心者も学びやすいフレームワークです。
Next.jsではサーバーサイドでの処理も簡単に書けるようになっており、
/pages/apiフォルダにファイルを置くだけでAPIの構築が可能です。
これにより、データベースとの連携なども簡単に実装できます。
Vercelとは
Vercel(バーセル)は、Next.jsの開発元が提供するクラウドプラットフォームで、
作成したウェブアプリを簡単にデプロイ(インターネット上に公開)できるサービスです。
VercelはNext.jsの公式提供元でもあり、Next.jsで作られたアプリケーションを非常にスムーズにデプロイできるように作られています。
GitHubなどのソースコード管理サービスと連携して、ボタン1つでプロジェクトをVercelにデプロイできます。
また、変更をコードに加えGitHubにプッシュすると自動的に更新が反映されるため、運用が便利になっています。
API実装
それでは実際に簡単なAPIを作成してみましょう!
①Next.jsプロジェクト作成
まずはクライアント側のアプリケーションを作成していきます
npm install -g pnpm
高速なpnpmをインストール
npx create-next-app@latest test-api --use pnpm
test-apiというプロジェクトを作成
※途中で出てくる項目はエンターキーを押してスキップでOKです
pnpm i
プロジェクトのパッケージをインストール
pnpm dev
開発サーバを起動
http://localhost:3000にアクセスし画面が表示されたらプロジェクトの作成は完了です
デプロイのため、Githubにリポジトリを作成し、上記のプロジェクトをプッシュしてください
②Vercelのセットアップ
次に作成したアプリケーションをサーバにアップロードするために、Vercelの設定を行なっていきます
・Vercelアカウントを作成する
画面の「Deploy now」をクリックしVercelへ移動
「Continue with GitHub」を選択しGithubアカウントと連携
リポジトリが表示されるので、pushした「test-api」インポート
・Vercelにプロジェクトを接続しデプロイする
インポートされたリポジトリを選択し、「Deploy」をクリックしてプロジェクトをデプロイ
③Vercel内でデータベースを作成
デプロイ後画面の「Continue to Dashboard」をクリック
「Strage」→「Connect Database」 →「Create New」の順でクリックし「Postgres」を選択
以下の画面で「Create」をクリック 作成後に「Connect」でデプロイしたリポジトリに接続
・Next.jsプロジェクトでDBを設定
test-apiディレクトリ内に.envファイルを作成
.env.localタブを開き、「Show Seecret」をクリックし表示された内容を.envファイルにコピペ
ターミナルで
pnpm i @vercel/postgres
を実行し、Vercel Postgres SDKをインストールします。
・データベースに初期データをシードする
Vercelに戻り、画面左端の「Data」をクリックしQueryタグを開く
以下のSQLを貼り付け実行
CREATE TABLE fruits ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL );
INSERT INTO fruits (name) VALUES ('Apple'),('Grape'), ('Banana');
Browseタブからfruitsテーブルとその中身を確認できます
④APIを実装
APIを作成する際に便利なルートハンドラーを使用して実装していきます!
npm install pg
データベースに接続するためのパッケージをインストール
test-apiディレクトリに apiフォルダ、その中にroute.tsを作成
以下のように記載します
route.ts
import pg from "pg";
import { NextResponse } from "next/server";
const { Pool } = pg;
// データベース接続プールを設定
const pool = new Pool({
connectionString: process.env.POSTGRES_URL, // 環境変数から接続情報を取得
});
// GETリクエストを作成
export async function GET() {
try {
const res = await pool.query("SELECT id, name from fruits");
return NextResponse.json({ fruits: res.rows });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to fetch data",
},
{ status: 500 }
);
}
}
次に、page.tsを以下のように書き換えます
page.ts
"use client";
import { useState, useEffect } from "react";
export default function Home() {
const [fruits, setFruits] = useState<{ id: number; name: string }[]>([]);
const [newFruit, setNewFruit] = useState<string>(""); // 新規追加用のステート
const [error, setError] = useState<string | null>(null);
// フルーツ名の変更を反映するハンドラ
const handleFruitChange = (index: number, newValue: string) => {
const updatedFruits = [...fruits];
updatedFruits[index] = {
...updatedFruits[index],
name: newValue,
};
setFruits(updatedFruits);
};
const data = await response.json();
if (response.ok) {
console.log("Added fruit:", data.fruit);
} else {
console.error("Error:", data.error);
}
}
useEffect(() => {
const fetchFruits = async () => {
try {
const res = await fetch("/api/fruits"); //先程作成したAPI
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Something went wrong");
}
setFruits(data.fruits);
} catch (error) {
if (error instanceof Error) {
setError(error.message);
} else {
setError("An unknown error occurred");
}
}
};
fetchFruits();
}, []);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<h1>APIテスト</h1>
{error && <p style={{ color: "red" }}>{error}</p>}
<ul>
{fruits.length > 0 ? (
fruits.map((fruit, index) => (
<li key={fruit.id}>
<input
type="text"
value={fruit.name}
onChange={(e) => handleFruitChange(index, e.target.value)}
className="border-2 border-white bg-black text-white p-2 mb-5 mr-5"
style={{ borderRadius: "4px" }}
/>
{/* 編集ボタン */}
<button
className="border-2 border-green-500 text-green-500 px-4 py-1 mr-2 rounded hover:bg-green-500 hover:text-white transition"
// onClick={() => updateFruit(fruit.id, fruit.name)}
>
編集
</button>
{/* 削除ボタン */}
<button
className="border-2 border-red-500 text-red-500 px-4 py-1 rounded hover:bg-red-500 hover:text-white transition"
// onClick={() => handleDeleteFruit(fruit.id)}
>
削除
</button>
</li>
))
) : (
<p>データを取得しています...</p>
)}
</ul>
{/* 新規フルーツ追加フォーム */}
<form className="flex items-center gap-4">
<input
type="text"
value={newFruit}
onChange={(e) => setNewFruit(e.target.value)}
placeholder="新しいフルーツ"
className="border-2 border-white bg-black text-white p-2"
style={{ borderRadius: "4px" }}
/>
<button
type="submit"
// onClick={() => {
addFruit(newFruit);
}}
className="border-2 border-blue-500 text-blue-500 px-4 py-2 rounded hover:bg-blue-500 hover:text-white transition"
>
新規追加
</button>
</form>
</main>
</div>
);
}
この状態でhttp://localhost:3000にアクセスすると、DBのデータの取得、表示が確認できます
DBとデータのやり取りを行うPOST、PUT、DELETEの処理を作成
route.tsとpage.tsを以下のように書き換えます
route.ts
import pg from "pg";
import { NextResponse } from "next/server";
const { Pool } = pg;
// データベース接続プールを設定
const pool = new Pool({
connectionString: process.env.POSTGRES_URL, // 環境変数から接続情報を取得
});
// GETリクエストを作成
export async function GET() {
try {
const res = await pool.query("SELECT id, name from fruits");
return NextResponse.json({ fruits: res.rows });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to fetch data",
},
{ status: 500 }
);
}
}
// POSTリクエストを作成
export async function POST(request: Request) {
try {
const { newFruit } = await request.json();
const res = await pool.query(
"INSERT INTO fruits (name) VALUES ($1) RETURNING *",
[newFruit]
);
return NextResponse.json({ fruit: res.rows[0] }, { status: 201 });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to insert data",
},
{ status: 500 }
);
}
}
// PUTリクエストを作成
export async function PUT(request: Request) {
try {
const { id, updatedFruits } = await request.json();
const res = await pool.query(
"UPDATE fruits SET name = $1 WHERE id = $2 RETURNING *",
[updatedFruits, id]
);
return NextResponse.json({ fruit: res.rows[0] }, { status: 200 });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to update data",
},
{ status: 500 }
);
}
}
/// DELETEリクエストを作成
export async function DELETE(request: Request) {
try {
const { id } = await request.json();
const res = await pool.query(
"DELETE FROM fruits WHERE id = $1 RETURNING *",
[id]
);
return NextResponse.json({ fruit: res.rows[0] }, { status: 200 });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to delete data",
},
{ status: 500 }
);
}
}
page.ts
"use client";
import { useState, useEffect } from "react";
export default function Home() {
const [fruits, setFruits] = useState<{ id: number; name: string }[]>([]);
const [newFruit, setNewFruit] = useState<string>(""); // 新規追加用のステート
const [error, setError] = useState<string | null>(null);
// フルーツ名の変更を反映するハンドラ
const handleFruitChange = (index: number, newValue: string) => {
const updatedFruits = [...fruits];
updatedFruits[index] = {
...updatedFruits[index],
name: newValue,
};
setFruits(updatedFruits);
};
// フルーツ追加用のハンドラ
async function addFruit(newFruit: string) {
console.log(newFruit);
const response = await fetch("/api/fruits", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ newFruit }),
});
const data = await response.json();
if (response.ok) {
console.log("Added fruit:", data.fruit);
} else {
console.error("Error:", data.error);
}
}
//フルーツ更新用のハンドラ
async function updateFruit(id: number, updatedFruits: string) {
const response = await fetch("/api/fruits", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id, updatedFruits }),
});
const data = await response.json();
if (response.ok) {
console.log("Updated fruit:", data.fruit);
window.location.reload();
} else {
console.error("Error:", data.error);
}
}
// フルーツ削除用のハンドラ
async function handleDeleteFruit(id: number) {
const response = await fetch("/api/fruits", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id }),
});
const data = await response.json();
if (response.ok) {
console.log("Updated fruit:", data.fruit);
window.location.reload();
} else {
console.error("Error:", data.error);
}
}
useEffect(() => {
const fetchFruits = async () => {
try {
const res = await fetch("/api/fruits"); //先程作成したAPI
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Something went wrong");
}
setFruits(data.fruits);
} catch (error) {
if (error instanceof Error) {
setError(error.message);
} else {
setError("An unknown error occurred");
}
}
};
fetchFruits();
}, []);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<h1>APIテスト</h1>
{error && <p style={{ color: "red" }}>{error}</p>}
<ul>
{fruits.length > 0 ? (
fruits.map((fruit, index) => (
<li key={fruit.id}>
<input
type="text"
value={fruit.name}
onChange={(e) => handleFruitChange(index, e.target.value)}
className="border-2 border-white bg-black text-white p-2 mb-5 mr-5"
style={{ borderRadius: "4px" }}
/>
{/* 編集ボタン */}
<button
className="border-2 border-green-500 text-green-500 px-4 py-1 mr-2 rounded hover:bg-green-500 hover:text-white transition"
onClick={() => updateFruit(fruit.id, fruit.name)}
>
編集
</button>
{/* 削除ボタン */}
<button
className="border-2 border-red-500 text-red-500 px-4 py-1 rounded hover:bg-red-500 hover:text-white transition"
onClick={() => handleDeleteFruit(fruit.id)}
>
削除
</button>
</li>
))
) : (
<p>データを取得しています...</p>
)}
</ul>
{/* 新規フルーツ追加フォーム */}
<form className="flex items-center gap-4">
<input
type="text"
value={newFruit}
onChange={(e) => setNewFruit(e.target.value)}
placeholder="新しいフルーツ"
className="border-2 border-white bg-black text-white p-2"
style={{ borderRadius: "4px" }}
/>
<button
type="submit"
onClick={() => {
addFruit(newFruit);
}}
className="border-2 border-blue-500 text-blue-500 px-4 py-2 rounded hover:bg-blue-500 hover:text-white transition"
>
新規追加
</button>
</form>
</main>
</div>
);
}
上記の内容でファイルを書き換えることで、
データの追加、更新、削除ができることを確認できるかと思います
Vercelにデプロイ時の注意点
※Ts-lintに引っかかっていたらビルドエラーになるため、未使用変数やパッケージインストール時に「npm i –save-dev @types/pg」のようにインストールしているかなど
ビルド前の確認が大切です!
※エラー回避のためにpnpm install –no-frozen-lockfile上記を実行しておくこともお忘れなく
最後に
Next.jsとVercelを使用することでさくっととAPIを実装、試せるのはとても便利でした!
また、Next.jsのみでクライアント処理もサーバー側の処理も一括して作成できるのは相当な強みであると同時にAPIについての理解の助けになるのではないかと思います。
実戦に勝る学習はありませんから、より理解を含めてみようという方はぜひ!
Next.jsとVercelでさくっと学習してみてくださいー!