TypeScript入門実践編②Next.js(React)+TypeScriptでデモを作ってみた!

TypeScript入門実践編TypeScriptでデモを作ってみた!シリーズ第2弾はNext.js(React)+TypeScript。TypeScript界隈でもっとも選ばれているフルスタックフレームワークです。前回のLaravel+Bladeとは違い、サーバー側の処理(データ取得・保存)も、画面側の表示も、すべてNext.jsという1つのフレームワークの中で完結します。
この記事の対象読者
- ①(Laravel+Blade+TS)を読んで、Next.jsとの違いを知りたい人
- 「TypeScript=Next.js」というイメージはあるが、実際に手を動かしたことがない人
- App RouterのServer Components・Server Actionsを、シンプルな題材で体験してみたい人
- 前回記事(20選・4つ構成比較)を読んで、実際の構築手順を知りたい人
この記事のゴール
この記事のゴールは、前回のLaravelと構成同じく、TypeScriptとNext.jsを使った、ごくシンプルな商品編集ページの制作です。表示して、保存を押すと更新される、それだけ。MySQLの商品テーブル(products)は、前回作成したts_demoをそのまま流用します。機能を絞ることで、①との構成そのものの違いに集中できるようにしています。
今回はルーティングにApp Router、DB接続にはmysql2を使った素のSQLを採用しました。Prismaのようなスキーマ管理ツールも選択肢にありましたが、今回のような単純な1件取得・1件更新だけの処理には過剰装備と判断し、あえてシンプルな構成を選んでいます。
※Next.jsは「サーバーで動く部分」と「ブラウザで動く部分」をファイルレベルで明確に分ける設計になっているため、①よりファイル数は増えます。最初は戸惑うかもしれませんが、その分け方自体がApp Routerの核心なので、ぜひ実際に手を動かしながら体感してみてください。
手順
| # | 手順 | 内容 | 使うツール |
|---|---|---|---|
| 1 | テーブル作成 | ts_demoデータベースにproductsテーブルを作成し、サンプルデータを投入 | SQL |
| 2 | プロジェクト作成 | create-next-appでdemo_nextjs_tsを作成(TypeScript、App Router、Turbopack等を選択) | npm |
| 3 | mysql2インストール・DB接続設定 | npm install mysql2、接続情報を.env.localに記載し、接続プールを作成 | npm |
| 4 | フォルダとファイルをつくる | 今回の商品編集機能に必要な、フォルダとファイルを作成 | |
| 5 | データ取得処理を実装 | 商品編集ページのServer Component内でmysql2を使いSELECT | コードエディタ |
| 6 | データ更新処理を実装 | 商品編集フォームのPOST処理を実装 | コードエディタ |
| 7 | フォームとServer Action作成 | 保存処理(UPDATE文)をactions.tsに実装 | コードエディタ |
| 8 | フォームとServer Actionを接続 | <form action={updateProduct}>で紐付け | コードエディタ |
| 9 | 動作確認(表示・更新) | ブラウザで表示→編集→保存の一連の流れを確認 | ブラウザ |
| 10 | 保存ボタンをClient Component化 | 送信前に確認ダイアログを出す処理を実装 | コードエディタ |
| 11 | 動作確認(確認ダイアログ) | 保存ボタン押下時にダイアログが出て、OKを押した場合のみ更新されることを確認 | ブラウザ |
事前準備
実装に入る前に以下の準備をお願いします。
1.MySQL(MariaDB)のインストール
2.VS Codeのインストール
3.Node.jsのインストール(バージョン確認:node -v、npm -v)
実装手順解説
1.テーブル作成
まず、productsテーブルを作成します。①ですでに作成済の方はスキップしてください。
1.お使いのMySQLクライアント(HeidiSQL、phpMyAdmin、コマンドラインのmysql等)で、以下のSQLを実行します。
CREATE DATABASE IF NOT EXISTS ts_demo;
USE ts_demo;
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price VARCHAR(255) NOT NULL
);
INSERT INTO products (name, price) VALUES
('ボールペン', '150'),
('ノート', '300');2.実行後、productsテーブルに2件のデータが入っていれば成功です。
2.プロジェクト作成
Next.jsは①のLaravelと違い、XAMPP(Apache)を経由しないので、C:\xampp\htdocsの中に置く必要はありません。今回はC:\projectsの中に、demo_nextjs_tsという名前でプロジェクトを作成します。
手順
1.VS Codeを開きます。
2.「ファイル」→「フォルダーを開く」を選択し、C:\xampp\htdocs を指定して開きます。
3.上部メニューの「ターミナル」→「新しいターミナル」を選択し、ターミナルを開きます。

4.以下のコマンドを実行します。
npx create-next-app@latest demo_nextjs_tsnpxとは、npm(Node.jsのパッケージ管理ツール)に同梱されている、パッケージ実行ツールです。①のComposerに近い立ち位置ですが、npxは「そのパッケージを、インストールせずに一時的に実行する」というのが特徴です。今回のcreate-next-appのように、プロジェクトを作る時だけ使い、その後は不要なツールに使われます。
create-next-appは、Vercel社(Next.jsの開発元)が公式に提供している、Next.jsプロジェクトの雛形(土台)を自動生成するツールです。①でComposerを使ってlaravel/laravelという雛形をダウンロードしたのと、同じ役割です。
@latestは、npmのパッケージ指定で使う書き方で、「そのパッケージの、現時点で最新のバージョンを使う」という指定です。バージョンを指定しない場合、古いキャッシュが使われてしまうことがあるため、@latestを付けておくことで、常に最新版のcreate-next-appを実行できます。
実行すると、プロジェクトの構成についていくつか質問が表示されます。実際の出力を貼り付けてください。
以下のように設定していきます。
Need to install the following packages:
create-next-app@16.2.10
Ok to proceed? (y) ←yを入力 (1)
? Would you like to use the recommended Next.js defaults? » - Use arrow-keys. Return to submit.
> Yes, use recommended defaults
TypeScript, ESLint, No React Compiler, Tailwind CSS, No src/ directory, App Router, AGENTS.md
No, customize settings ←Noを選択 (2)
Would you like to use TypeScript? » No / Yes ←Yesを選択 (3)
? Which linter would you like to use? » - Use arrow-keys. Return to submit.
> ESLint - More comprehensive lint rules ←ESLintを選択 (4)
Biome
None
? Would you like to use React Compiler? » No / Yes ←Noを選択 (5)
? Would you like to use Tailwind CSS? » No / Yes ←Noを選択 (6)
? Would you like your code inside a `src/` directory? » No / Yes ←Noを選択 (7)
? Would you like to use App Router? (recommended) » No / Yes ←Yesを選択 (8)
? Would you like to customize the import alias (`@/*` by default)? » No / Yes ←Noを選択 (9)
? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? » No / Yes ←Noを選択(10)各選択肢の意味は以下のとおり。
| # | 設定項目 | 選択内容 | 理由 |
|---|---|---|---|
| 1 | パッケージのインストール確認 | y | create-next-app本体を取得するため |
| 2 | 推奨デフォルトを使うか | No, customize settings | Tailwind CSS等、個別に選びたい項目があるため |
| 3 | TypeScriptを使うか | Yes | 今回のシリーズの本題のため |
| 4 | Linter(コードの書き方チェックツール) | ESLint | 定番かつ標準の選択肢。TypeScriptの型チェックとは役割が異なり併用が一般的 |
| 5 | React Compiler(レンダリング自動最適化の実験的機能) | No | まず標準的な構成で進めたいため |
| 6 | Tailwind CSS | No | ①のBladeとの対比を重視し、素のCSSで進めるため |
| 7 | src/ディレクトリを使うか | No | app/をルート直下に置き、記事内のパス表記をシンプルにするため |
| 8 | App Router | Yes | ②の技術方針として当初から決定済み |
| 9 | import時のパス指定(インポートエイリアス) | No(デフォルト@/*のまま) | 変更する理由がないため |
| 10 | AGENTS.md(AIコーディングエージェント向けガイド) | No | 人間(A様)が手を動かして進めるため不要 |
インストールが完了すると以下のフォルダが生成されます。

appフォルダがApp Routerのルーティングの起点、publicが画像等の静的ファイル置き場、node_modulesが依存パッケージ、package.jsonが①でいうcomposer.jsonに相当する管理ファイルです。tsconfig.jsonもすでに自動生成されているので、①のように手動で編集する作業は不要です。
5.正しく作成できたか確認します。以下のコマンドを実行してください。
npm run dev7.ブラウザでhttp://localhost:3000にアクセスし、Next.jsの初期画面が表示されれば、プロジェクト作成は成功です。

3.mysql2インストール・DB接続設定
商品データをMySQLから取得・更新できるように、mysql2パッケージを導入します。
手順
1.前の手順で実行したnpm run devは動かしたままにしたいので、新しくターミナルを開きます。ターミナルパネル右上の「+」アイコンをクリックするか、Ctrl + Shift + \``を押してください。プロジェクトのフォルダを開いた状態のまま新しいターミナルが開くので、フォルダを移動する必要はありません。
2.新しいターミナルで、以下を実行します。
npm install mysql2以下のようにmysql2パッケージがダウンロード、インストールされます。

3.プロジェクトのルート(demo_nextjs_ts直下)に、.env.localという名前のファイルを新規作成します。

4.中身に、DB接続情報を記載します。①と同じts_demoデータベースです。
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=ts_demo
DB_USER=root
DB_PASSWORD=このファイルは、Next.jsの仕様ですが自分で作る必要があります。
5.DB接続用の共通ファイルを作成します。
/libというフォルダを作成し、その中にdb.tsという名前のファイルを新規作成します。

import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
});
export default pool;これは、Next.js、mysql2の仕様ではなく、DB接続を読み込むコードを共通化しているだけです。後述するコードでこのファイルをインポートします。
4.フォルダとファイルをつくる
今回の商品編集機能に必要な、4つのファイルを作成します。Next.jsにはフォルダの構造そのものがURLのルーティングになる、App Routerという仕組みがあります。Laravelでいう、routes/web.phpへのルート追加に相当する作業を、この仕組みを使って進めます。
手順
1.以下の図のようにフォルダとファイルを作成してください。
app\products\[id]\edit\page.tsxfeatures\products\dal\products.tsfeatures\products\views\edit\product-edit-view.tsxfeatures\products\actions\update-product.ts

POINT
Next.jsのフォルダ構成はルールがほぼ決まっていないので逆に構築してよいか露頭に迷う場合もあります。自作でつくってあとから作り直しになるなんてことも。
そこで、Next.jsのシステム構成図をまとめましたのでよかったらこちらも合わせてが
5.データ取得処理を実装
dal(DBアクセス)、page.tsx(呼び出し役)、views(画面表示)の3つを実装していきます。
手順
1.features\products\dal\products.tsに、以下を記述します。
import 'server-only';
import pool from '@/lib/db';
// 商品を1件取得する
export async function getProduct(id: string) {
const result = await pool.query('SELECT * FROM products WHERE id = ?', [id]);
const rows = result[0];
const product = rows[0];
return product;
}1行目のimport 'server-only'は、このファイルがブラウザ側のコード(Client Component)に誤って混ざってしまった場合、Next.jsがビルド時にエラーとして検知してくれる仕組みです。DBのパスワードやSQLがブラウザ側に漏れることを防ぎます。
import pool from '@/lib/db'は、以前作成したDB接続プールを読み込んでいます。
getProduct関数の中のpool.query(...)は、mysql2ライブラリPoolクラスのインスタンスメソッドで、戻り値はPromiseです。実行結果は[取得データ本体, フィールド情報]という2つの要素を持つ配列で返ってくるため、1番目(result[0])が実際のデータです。そのデータ本体も、複数件に対応できるよう配列の形になっているため、rows[0]で1件目だけを取り出しています。
2.features\products\views\edit\product-edit-view.tsxに、以下を記述します。
export default function ProductEditView(props: { product: any }) {
return (
<div>
<h1>商品編集</h1>
<p>{props.product.name}</p>
<p>{props.product.price}</p>
</div>
);
}商品編集画面をつくります。propsは、このコンポーネントを呼び出す側(page.tsx)から渡されるデータを受け取る、Reactの仕組みです。今回はproductという商品データを渡してもらう想定で、props.product.nameのように、波カッコで値を画面に埋め込んでいます。
3.app\products\[id]\edit\page.tsxに、以下を記述します。
import { getProduct } from '@/features/products/dal/products';
import ProductEditView from '@/features/products/views/edit/product-edit-view';
// 編集ページを表示するメソッド
export default async function EditPage({ params }: { params: Promise<{ id: string }> }) {
const paramsData = await params;
const id = paramsData.id;
const product = await getProduct(id);
return <ProductEditView product={product} />;
}URLをアクセスした時に最初に実行されるファイルです。paramsはPromise型で渡ってくるため、awaitで中身を取り出し、その中のidを使ってgetProductを呼び出しています。取得したproductを、<ProductEditView product={product} />という書き方で、先ほど作ったProductEditViewコンポーネントに渡しています。
4.ブラウザで以下のURLにアクセスします。
http://localhost:3000/products/1/edit
5.1件目の商品(ボールペン)の名前と価格が表示されたら成功です。

6.データ更新処理を実装
つづいて、保存処理(actions)を実装します。
手順
1.features\products\dal\products.tsに、更新用の関数を追加します。
export async function updateProduct(id: string, name: string, price: string) {
await pool.query('UPDATE products SET name = ?, price = ? WHERE id = ?', [name, price, id]);
}getProductと同じpool(DB接続プール)を使い、UPDATE文を実行しています。SQLの?の部分に、配列で渡したname・price・idが、順番に安全に埋め込まれます。
2.features\products\actions\update-product.tsに、以下を記述します。
'use server';
import { revalidatePath } from 'next/cache';
import { updateProduct as updateProductInDb } from '@/features/products/dal/products';
export async function updateProduct(id: string, prevState: any, formData: FormData) {
const name = formData.get('name') as string;
const price = formData.get('price') as string;
await updateProductInDb(id, name, price);
revalidatePath(`/products/${id}/edit`);
return { message: '更新しました' };
}1行目の'use server'は、この関数がサーバー専用の処理(Server Action)であることを示す、Next.jsの構文です。この記述があるファイルの関数は、ブラウザ側から直接呼び出せるようになります。
import { updateProduct as updateProductInDb }は、dal側のupdateProductと、このファイルのupdateProductが同じ名前で衝突するため、updateProductInDbという別名で読み込んでいます。
formData.get('name')は、後で作るフォームから送信された、nameという名前の入力欄の値を取り出しています。取り出した値をもとに、dal側の更新関数を呼び出しています。
revalidatePath('/products/1/edit')のように、更新した内容が反映されるべきURLを指定すると、Next.jsがそのページのキャッシュを破棄し、次回表示時は最新のDBの値で作り直してくれます。import { revalidatePath } from 'next/cache'は、Next.js自身が提供するキャッシュ操作用の関数です。
useActionStateという仕組みを使う場合、Server Actionは(prevState, formData)という形で、直前の結果(prevState)も一緒に受け取る決まりになっています。
7.フォームとServer Action作成
views側の画面を、<form>とServer Actionで実際に更新できるようにします。
手順
1.features\products\views\edit\product-edit-view.tsxを、以下のように書き換えます。
'use client';
import { useActionState } from 'react';
import { updateProduct } from '@/features/products/actions/update-product';
export default function ProductEditView(props: { product: any }) {
const updateProductWithId = updateProduct.bind(null, props.product.id);
const [state, formAction] = useActionState(updateProductWithId, null);
return (
<form action={formAction}>
<h1>商品編集</h1>
<input type="text" name="name" defaultValue={props.product.name} />
<input type="text" name="price" defaultValue={props.product.price} />
<button type="submit">保存</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}<form action={...}>にJavaScriptの関数を直接渡せるのは、Server Actionの仕様です。フォームが送信されると、Next.jsが自動的にこの関数を呼び出し、入力された内容をFormDataとして渡してくれます。
updateProduct.bind(null, props.product.id)は、updateProduct(id, formData)という2つの引数のうち、idの方をあらかじめ固定しておく書き方です。フォーム送信時にはformDataだけが自動的に渡されるため、idは先に埋め込んでおく必要があります。
<input>タグのname属性(name・price)が、送信されるFormDataのキーになります。defaultValueで、現在のDBの値を初期表示しています。
2. 画面を再表示して確認。
画面を更新(F5)すと、以下のように表示されると思います。

3. 更新処理をチェック
商品名、値段を入れて保存を押すと「更新しました。」と表示されたら成功です。

8.保存ボタンをClient Component化
最後に、保存ボタンを押した時に、「この内容で保存しますか?」という確認ダイアログを表示するようにします。
手順
1.features\products\views\edit\product-edit-view.tsxを、以下のように書き換えます。
'use client';
import { useActionState } from 'react';
import { updateProduct } from '@/features/products/actions/update-product';
export default function ProductEditView(props: { product: any }) {
const updateProductWithId = updateProduct.bind(null, props.product.id);
const [state, formAction] = useActionState(updateProductWithId, null);
function handleSubmit(event: React.FormEvent) {
const isConfirmed = confirm('この内容で保存しますか?');
if (!isConfirmed) {
event.preventDefault();
}
}
return (
<form action={formAction} onSubmit={handleSubmit}>
<h1>商品編集</h1>
<input type="text" name="name" defaultValue={props.product.name} />
<input type="text" name="price" defaultValue={props.product.price} />
<button type="submit">保存</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}onSubmit={handleSubmit}を<form>タグに追加しています。handleSubmit関数は、フォーム送信のタイミングでconfirm()(ブラウザ標準の確認ダイアログ)を表示し、「キャンセル」が押された場合はevent.preventDefault()で送信そのものを止めます。action(Server Actionへの送信)とonSubmit(送信前のチェック)は、それぞれ別の役割で共存できます。

9.動作確認(確認ダイアログ)
一連の流れを、最終チェックします。
手順
1.ブラウザで以下のURLにアクセスします。
http://localhost:3000/products/1/edit
2.商品名または価格を書き換えます。
3.「保存」ボタンを押します。
4.「この内容で保存しますか?」という確認ダイアログが表示されることを確認します。
5.「キャンセル」を押した場合、保存されずに元の画面のままであることを確認します。
6.もう一度「保存」を押し、今度は「OK」を押します。
7.「更新しました」というメッセージが表示され、書き換えた内容がそのまま画面に反映されていることを確認します。
すべて問題なく確認できたら、今回の商品編集ページの実装は一通り完成です。お疲れ様でした!
さいごに
今回は、Next.js(App Router)+TypeScriptという組み合わせで、商品編集ページを実際に構築してみました。
特に、「Next.jsはフォルダ構成に公式の正解が定まっていない」という点は、実際に手を動かしてみて、初めて実感できたことだったと思います。①のLaravelはMVCという規約が最初から用意されていたのに対し、②のNext.jsはfeatures・dal・viewsといった構成を、根拠を確認しながら自分たちで組み立てる必要がありました。配属される現場によって、採用しているフォルダ構成が大きく異なる可能性がある、という点は、これからNext.jsに触れる方には、ぜひ覚えておいてほしいポイントです(詳しくは別記事「Next.jsシステム構成図 2026版」でも掘り下げています)。
次回は、③Nuxt(Vue)に進みます。同じproductsテーブル、同じ商品編集ページという題材で、今度はVueというReactとは異なるUIライブラリを土台にした場合、どんな違いが見えてくるか、引き続き検証していきます。


