2024-3I プログラミング3 第08回 講義資料

2024年12月12日(木)1・2時限

1 連絡・概要

1.1 連絡

後期中間試験、お疲れ様でした。後期中間成績は 小テスト➊~➌20%課題1(Todoアプリ)を 80% の割合で総合して評価します (シラバス記載の評価割合です)。課題1は、README.md を3点、アプリ本体を7点の合計「10点満点」で評価しています (7.5点が標準評価になります)。

1.2 案内

1.3 就活・コンテスト応募に提出するリポジトリの「README」について

リポジトリの README について、次のチェックリストに「3個以上該当する」と、就活 (インターン) やコンテストの応募では「アプリを起動してもらえない」または「コードを見てもらえない」という可能性が高まります。採用担当者や審査員に対する アピールが弱いREADME、やっつけ仕事のREADME では、それだけで選考落ちしてしまうリスクがあります。

READMEは、選考の「プレゼン資料」であると位置づけて、より良いものになるように仕上げてください。また、生成AIを使って添削や校正を受けるようにしてください (そのうえで先生にアドバイスをもらうと良いです)。

1.4 今後の授業の流れ

前回講義ではmicroCMSを使ってブログアプリの「バックエンド」を構築・構成しました。ここからの授業では、Next.js を使って自前でバックエンドを実装していきます。

具体的には、データ永続化、ウェブAPI、ユーザ認証 (外部サービス利用)、投稿記事やカテゴリのCRUD操作機能 (フロントエンドによるフォームの実装も含む) などの実装を進めていきます。まずは、リレーショナルデータベース (RDB) を使ったバックエンドサイドの「データの永続化」からはじめていきます。

ここから、また一段と難易度が高くなりますが、頑張っていきましょう🔥なお、繰り返しになりますが、本科目は学修単位科目であり、1回の授業あたり「4時間相当の授業時間外学習」をすることを前提 としたボリュームと進行速度となっています。1週間の間隔をあけての90分の取り組みでは理解・習得できるものではないので、適宜、復習に努めてください。

「バックエンド」や「データの永続化」という概念は既に解説済みですが、もし、理解に自信がない場合は生成AIを使って再確認しておいてください。

(プロンプト例)

ウェブアプリ開発の文脈における「バックエンド」とは何ですか。

(プロンプト例)

ウェブアプリ開発の文脈における「データの永続化」とは何ですか。

リレーショナルデータベース (RDB) については、2年次の「情報2」の第13回と第14回でも取り上げています。講義資料と教科書 (キタミ式ITパスポート) の p.160 ~ p.191 もあわせて復習しておくことをお勧めします。データベースに関して理論を含めた専門的な内容については、4年生の後期開講の「データベース工学 (学修単位・2単位)」で学びます。

2 データの永続化

第04回講義第05回講義のなかで取り組んだ「Todoアプリの開発」では、ウェブブラウザの機能であるローカルストレージ(Window.localStorage API) を使用して「データの永続化」を実装しました。つまり、フロントエンドでデータの永続化を行なっていました。

しかし、ローカルストレージは ブラウザが管理する領域にデータを保存する方式 であるため、それを利用したTodoアプリでは「PCから登録したタスクを、スマートフォンから確認する」といった使い方は、で・き・ま・せ・ん・でした😭

また、同じ PC からアクセスしても「Chromeを使ったとき」と「Edgeを使ったとき」でデータを共有することができませんでした。本来「インターネットに接続可能な環境であればデバイスや場所を選ばずに利用できる」というのがウェブアプリのメリットなのですが、ローカルストレージを使ったデータの永続化では、残念ながらこのメリットを活かすことはできませんでした。

このようなことから、一般的なウェブアプリでは バックエンドに配置したデータベースなど使ってデータの永続化 が行なわれます。この方式であれば、先の問題の解決に加えて、次のようなメリットが得られます。

第一に、複数のユーザでデータを共有できるようになります。例えば、チームでタスク (Todo) を管理する場合、メンバー全員が同じタスクリストを参照し、更新内容をリアルタイムで共有することができます。

第二に、大量のデータを安全に保管できるようになります。ローカルストレージでは、その仕様上、保存容量に制限 (=文字列形式で約5MB) がありましたが、データベースを使えばテラバイト単位のデータも扱うことができ、なおかつ バックアップ などの堅牢な仕組みによってデータの消失を防ぐことも可能になります。

また、(リレーショナルデータベースとは別の仕組みになりますが) オブジェクトストレージ関連のウェブサービス (Amazon S3や Azure Blob Storage、GCP Cloud Storage) を利用すれば画像、音声、動画など任意のバイナリデータを扱うことも可能になります。

ここからの「ブログアプリ開発」では、以上のメリットを活かすために、バックエンドでリレーショナルデータベース (RDB) をデータを永続化する機能を実装していきます。

2.0.1 定着確認

3 データベースについて

データベースのアーキテクチャ (内部の仕組み/データの整理方法) は、大きく分けると「RDB (リレーショナルデータベース)」と「NoSQL」の2つに分類されます。

ここで取り組む「ブログアプリの開発」には、リレーショナルデータベース を利用していきます。

3.1 NoSQL

NoSQL (ノーエスキューエル) の代表的なものとして「ドキュメント指向DB (ドキュメント型DB)」があります。ドキュメント指向DB は、ウェブブラウザのローカルストレージと同じように 固定的なスキーマを必要とせず 「JSON形式」や「XML形式」 でデータを自由に格納できるという特徴を持っています。

ここで「固定的なスキーマを必要としない」とは データのスキーマ (構造・型) が統一されていなくても良い ということを意味します。例えば、ドキュメント指向DBには、以下のように 異なるスキーマを持った レコード (データ) を「同じ保存領域」に格納すること ができます。

{
  "id": "user1",
  "name": "寝屋川タヌキ",
  "age": 5,
  "hobbies": ["どんぐり収集", "毛づくろい"]
}
{
  "id": "user2",
  "name": "萱島ウサギ",
  "email": "abbit@example.com",
  "skills": {
    "jump": 3,
    "speed": 2,
    "stealth": 4 
  }
}

1件目のレコードは agehobbies という属性を持ち、2件目のレコードは、それとは異なる emailskills という属性を持っています。このようにドキュメント指向DBでは レコード (データ) ごとに異なる項目や構造を持たせること が可能となっています。また 配列や階層構造 (ネスト構造) のデータ が柔軟に格納できることも大きな特徴となっています。

さらに、ほとんどのドキュメント指向DBが JSON形式 (JavaScript Object Notation 形式) / BSON に対応しており、JavaScript / TypeScript との親和性が高く、ウェブアプリ開発で採用されることが増えています。その場合、フロントエンドからバックエンドまで一貫してJSON形式でデータをやり取りできるため、データの変換作業が最小限で済む というメリットがあります。

(プロンプト例)

ドキュメント指向データベースの文脈において「BSON」について解説してください。

一方で、後述の RDB では、同じテーブル (表) に格納する全てのレコードは「共通のカラム (列)」 を持つことが要求されます。さらに、そのテーブルのスキーマ (カラムの名前、データ型、値の制約など) は レコードを登録する前に設計しておく必要がある という原則があります。

ドキュメント指向DBの代表的なプロダクトとしてはMongoDB(モンゴディービー) やCouchDB(カウチディービー) があります。また、ドキュメント指向DBの特性を活かした BaaS (= Backend as a Service) としてGCP FirebaseAmazon DynamoDBMongoDB Atlasなどのクラウドサービスがあり、様々なモバイルアプリやウェブサービスにおいて利用されています。

この他、NoSQLとしては「キーバリューストア型DB」や「グラフ型DB (ネットワーク型DB)」などのデータベースも存在します。興味がある学生は、生成AIやウェブ検索を利用して概要を把握しておいてください。

(プロンプト例)

ドキュメント指向DB、キーバリューストア型DB、グラフ型DBの概要を説明してください。

(プロンプト例)

ウェブアプリ開発の文脈で「BaaS」とは何ですか。専門用語でできるだけ避けて、初心者向けに機能や採用のメリットなどを解説してください。

3.1.1 定着確認

3.2 RDB

RDB (Relational Database) は シンプルな「2次元の表形式」でデータを保持することが特徴 のDBです。近年では NoSQL の普及も進んでいますが、単に「DB」と言えば RDB を指すほど一般的であり、特に「銀行システム」や「ECサイト」などで広く採用されています。表形式によるデータ管理には多くのメリットがあるものの、可変長配列階層構造のデータ直接的に扱えない という欠点があります。

(プロンプト例)

プログラム開発者の観点では、オブジェクトデータの永続化が直接的に可能なドキュメント指向DBのほうが、RDBと比較してストレージとしてのメリットが大きいように考えます。しかし、実際にはRDBが採用されることが多いのは、どのような理由か解説してください。

例えば、JavaScript/TypeScriptで、次のようなオブジェクトデータを扱っているとします。このデータでは、第7行目第11行目のように「カテゴリ」に関する情報が 階層的な構造を持ち、その要素数が可変 (=例えば、2個のカテゴリをもつ場合もあれば、3個のカテゴリを持つ場合もある構造) となっています。

[
  {
    id: "p-001",
    title: "JavaScriptだと動いたのに…な毎日",
    content: "TypeScriptに移行してから…",
    createdAt: "2024-03-19",
    categories: [
      { id: "c-001", name: "プログラミング" },
      { id: "c-002", name: "TypeScript" },
      { id: "c-003", name: "JavaScript" },
    ],
  },
  {
    id: "p-002",
    title: "リストの[-1]ってマイナスなのになんで動くの?",
    content: "Pythonのリストで…",
    createdAt: "2024-03-24",
    categories: [
      { id: "c-001", name: "プログラミング" },
      { id: "c-004", name: "Python" },
    ],
  },
];

このようなオブジェクトデータを RDBを用いて永続化するためには 正規化 という処理を施して、次のように 複数のテーブル (表) に分解して扱う必要 があります。

■ posts テーブル

id title content created_at
p-001 JavaScriptだと… TypeScriptに移行… 2024-03-19
p-002 リストの[-1]って… Pythonのリストで… 2024-03-24

■ categories テーブル

id name
c-001 プログラミング
c-002 TypeScript
c-003 JavaScript
c-004 Python

■ posts_categories テーブル

post_id category_id
p-001 c-001
p-001 c-002
p-001 c-003
p-002 c-001
p-002 c-004

このテーブル設計において「posts_categoriesテーブル」は橋渡し的な役割を果たしています。このような複数のテーブルの「紐付け」を行なうテーブルを「中間テーブル」「Join Table」「Junction Table」と言います。1つの投稿記事が複数のカテゴリを持ち、なおかつ、1つのカテゴリが複数の投稿記事に属する という 多対多 (N:N) の関係性をRDBの形式で実現しています。このような変換をRDBでは データの正規化 (Normalization) と呼びます。正規化によって データの重複 を排除し、一貫性を保ちながら効率的なデータ管理 が可能になります。

RDBにおける「中間テーブル」や「多対多 (N:N)」の概念については「やさしい図解で学ぶ 中間テーブル 多対多 概念編」などを参照してください。詳しくは、4年生後期開講の「データベース工学」で学びます。

このように適切に正規化されたRDBでは、例えば、カテゴリ c-001 の「プログラミング」という名前を「コーディング」に変更したいとき、categoriesテーブルのなかの1件のレコードを更新するだけ で完了するという大きなメリットがあります。

一方で、RDBを使用すると「プログラムからRDBにデータを保存するとき」と「RDBからプログラムにデータを読み込むとき」に煩雑な変換処理が必要となります。具体的には、データを保存するときには オブジェクトの内容を、各テーブルに記録するようなクエリ (SQL文) を作成して、それをRDBに発行する必要 があります。逆に、読み込む際には、各テーブルから必要なデータを抽出するためのクエリ (SQL文) を作成してRDBに発行し、その実行結果 (平坦化されたレコードの配列) を元の階層構造を持つオブジェクト形式に再構築する必要があります。

SQL (エス・キュー・エル: Structured Query Language) は、RDB に対する クエリ (検索、取得、挿入、更新、削除などの一操作指示) を記述するための言語です。

このような「オブジェクトデータ」と「リレーショナルデータ」を接続する際の困難さを O/Rインピーダンス・ミスマッチ と呼びます。

(プロンプト例)

O/Rインピーダンスミスマッチに関する質問です。現在、TypeScriptを使ってブログアプリ(ブログ記事とカテゴリがN:Nの関係)を開発しています。先生から、RDBから取得したデータは「平坦化されたレコードの配列」であるため、それを元の「階層構造を持つオブジェクト形式」に再構築する必要がある、と言われました。このことについて具体的なイメージがつかめないので解説してください。変換のプログラムを知りたいのではなく、それぞれのデータ構造と変換のイメージを把握したいです。

3.3 ORM

O/Rインピーダンス・ミスマッチを解決するための概念・技術として Object-Relational Mapping (ORM) というものがあります。これは、オブジェクト指向プログラミングにおける「オブジェクトモデル」と、RDBにおける「リレーショナルモデル」の間の「溝」を埋めるための (違いを吸収するための) 設計パターンとなります。

この Object-Relational Mapping を実装したツール/ライブラリは Object-Relational Mapper(ORM) と呼ばれ、開発者がRDBを直接意識することなく(つまり、SQL文でクエリを記述することなく)、プログラミング言語の ネイティブなオブジェクトとしてデータを扱うことを可能 にします。

TypeScript/JavaScript には Prisma (プリズマ) という ORM があり、広く使われています。このブログアプリ開発でも、Prisma を最大限に利用して RDB とのデータのやりとりを行ないます。

3.4 Prisma のインストール

プロジェクトに Prisma(O/R Mapper) をインストールしていきます。VSCode でプロジェクト (next-blog-app) をオープンして、Ctrl+J でターミナルを開き、以下のコマンドを実行してください。

npm i -D prisma

また、データベースの「初期データ (Seed)」を作成する際にts-nodeを使用するので、こちらもプロジェクトのローカルにインストールしてください。

npm i -D ts-node

ts-node第01回講義でも解説したように TypeScriptを (JavaScriptに変換せずに) Node.js 上で 直接実行 するためのランタイム になります。

npmのアップデート

npmコマンドを実行した際に、次のように npm のアップデートを促すメッセージ が表示されたときは、適宜、指示に従ってアップデートしてください。

npm notice New minor version of npm available! 10.8.3 -> 10.9.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.1
npm notice To update run: npm install -g npm@10.9.1

具体的には、ターミナルから次のようなコマンドを実行してください。@ 以降のバージョン番号は、適宜、読み替えてください。

npm install -g npm@10.9.1

なお、-g は、グローバル環境を対象にインストール (アップデート) することを指示するオプションになります。第01回講義でも触れたように、npm は、通常、プロジェクトのローカル環境ではなく、グローバル環境にインストールします。

セキュリティ脆弱性に対する緊急対応

npmコマンドを実行した際に「XX high severity vulnerability」のように表示されたときは XX件の深刻なセキュリティ脆弱性 が検出されたことを意味します。インストール済みのパッケージのなかに緊急対応を要する脆弱性を持つものが存在し、それに対して対応を促すメッセージとなっています。

XX high severity vulnerability」というメッセージが表示されたときは、まずは以下のコマンドで詳細を確認してください。

npm audit

つづいて、次のコマンドで、当該パッケージをセキュティ対応したバージョンにアップデートします。成功すると「found 0 vulnerabilities」というメッセージが出力されます。

npm audit fix

なお、依存関係の問題などで npm audit fix では対応できない (解決できない) こともあります。その場合は、個別に情報収集して対応したり、生成AIを使って解決してください。

3.5 Prisma の初期化処理 (セットアップ処理)

次に Prisma を 使用するための準備 を行なっていきます。 この初期化処理は、プロジェクトのローカル環境に Prisma をインストール後、基本的に 初回のみに実行すればよい ものとなります。

VSCodeのターミナルから次のコマンドを実行してください。npm ではなく npx であることに注意してください。--datasource-provider sqlite のオプションにより、DB として SQLite (エスキューライト、スクライト) を使用する設定にするように指示しています。

なお、Prisma は「SQLite」の他に MySQL、PostgreSQL など様々な DB に対応しています。

npx prisma init --datasource-provider sqlite

このコマンドを実行すると、プロジェクトフォルダの直下に prisma というフォルダが作成され、さらに、そのなかに schema.prisma という 設定ファイル が作成されます。また、プロジェクトフォルダの直下の .env (前回講義環境変数の設定 を記述したファイル) に情報が追記がされます。

img

3.6 .env の内容確認

.env を開いて、Prismaの初期化処理によって追記された内容を確認してください。# から開始する行は コメント なので編集・削除しても問題ありません。

ファイルの最後のほうに DATABASE_URL という環境変数設定が追加され、そこに "file:./dev.db" がセット (代入) されていることが確認できると思います。

# microCMS
NEXT_PUBLIC_MICROCMS_BASE_EP=https://XXXXX.microcms.io/api/v1
NEXT_PUBLIC_MICROCMS_API_KEY=YYYYY

# prisma ▼▼ 追加されていることを確認 ▼▼
DATABASE_URL="file:./dev.db"

これは Prisma がデータベース接続に際して参照する環境変数 で、この値によって、Prisma は dev.db という「SQLiteのデータベースファイル (現時点ではまだ作成されていません)」に接続するようになります。

3.7 schema.prisma の内容確認

prisma フォルダのなかの schema.prisma を開いて内容を確認してください。// から開始する行は コメント なので編集・削除して問題ありません。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

第02行目 では、JavaScript (TypeScript) に対応した「Prisma Client」を生成することを指定しています。Prisma Client クラスから生成されるインスタンスを通して (SQLを記述することなく) TypeScript プログラムから DB のCURD操作 ができるようになります。

第06行目 では、使用するDB (Prismaが接続するDB) が「SQLite」であることを指定しています。

第07行目 では .env ファイルの DATABASE_URL から DB接続情報 を読み取るように指示しています。現状では、DATABASE_URLfile:./dev.db が設定されています。

また、schema.prisma は、データベースのスキーマ定義 (=テーブルやフィールドの構造定義) を記述するファイルとしての役割も持ちます。次のセクションでは ブログアプリに必要となる各テーブルのスキーマを定義していきます。

3.8 schema.prisma の編集

ブログアプリに必要な各種データ (投稿記事、カテゴリ) を保存するためのスキーマを schema.prisma に記述していきます。まず、schema.prisma を次のように書き換えて、保存してください。

// このファイルを更新したら...
// 0. `npm run dev` や `npx prisma studio` を停止
// 1. dev.db を削除
// 2. npx prisma db push
// 3. npx prisma generate
// 4. npx prisma db seed

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

// 投稿記事テーブル
model Post {
  id             String @id @default(uuid())
  title          String
  content        String
  coverImageURL  String
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
  categories     PostCategory[]
}

// カテゴリテーブル
model Category {
  id         String @id @default(uuid())
  name       String @unique
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  posts      PostCategory[]
}

// 投稿記事とカテゴリを紐づける中間テーブル
model PostCategory {
  id          String @id @default(uuid())
  postId      String
  categoryId  String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  category    Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
  post        Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
}

第17行目 以降で「投稿記事テーブル Post」「カテゴリテーブル Category」「投稿記事とカテゴリを紐づける中間テーブル PostCategory」の3つのテーブルのスキーマ (モデル) を定義しています。この定義に基づき、Prisma によって DB にテーブルが作成され、また、それらのテーブルを操作するための「メソッド」テーブルから取得される「データ型」 が TypeScript で利用できるようになります。

投稿記事テーブル (Post) の定義の 第22行目 では、今後の展開の都合で「src/app/_types/CoverImage.ts で定義したような urlheightwidth という属性を持った coverImage」ではなく、coverImageURL に変更している点に注意してください。

schema.prisma の詳細 (記述方法) については、ウェブ記事や生成AIを利用して概要だけ把握しておいてください。特に 第44行目第45行目 では特徴的な設定がされているので注意してください。

(プロンプト例)

TypeScript で Prisma を使用するにあたって、次のような schema.prisma を受け取りました。「投稿記事テーブル」のスキーマ定義について、丁寧に解説してください。特にカラムの「型」や「制約」について説明してください。

~ schema.prisma の内容を貼付け ~

つづいて「カテゴリテーブル」について同様に解説してください。

つづいて「中間テーブル」について同様に解説してください。

3.9 データベースの作成と確認

schema.prisma を編集・保存したら、次のコマンドを実行してください。

npx prisma db push

このコマンドにより、schema.prisma に基づいて 実際にデータベースが新規作成され (現在の設定の場合は dev.db というファイルが新規作成され)、その DB のなかに 「3つのテーブル」が作成 されます。既にデータベースが存在する場合は The database is already in sync with the Prisma schema. となります。

さらに、次のコマンドを実行してください。この操作により schema.prisma に基づき、プロジェクトのなかで参照可能なメソッドや型情報などが生成されます。

npx prisma generate

補足: 自動生成される「型」の参照

npx prisma generate を実行すると、プロジェクトのなかで schema.prisma に記述したスキーマ (モデル) に基づいて自動的に「型」が定義され、それを参照することができます。

例えば import { Category } from "@prisma/client"; とすれば、以下のようにカテゴリの型を参照できるようになります。

img

つづいて、VSCode から、次のように dev.db が作成されていることを確認してください。

img

Prismaによって作成されたDBの内容は「Prisma Studio」というツールを使って、ウェブブラウザから確認することができます。Prisma Studio は、VSCodeのターミナルから、以下のコマンドで起動することができます (デフォルトでは5555番ポートで起動します)。

npx prisma studio

Prisma Studio のトップ画面では、テーブル一覧 (モデル一覧) が表示されます。いまは「投稿記事のテーブル」を確認したいので「Post」をクリックしてください。

img

画面が切り替わり、下図のように「投稿記事テーブル (Post)」の「カラム (列) の構成」を確認することができます。現時点では、まだ投稿記事のレコードが1件も存在しませんが、レコード追加後は、その内容について確認・編集することもできます。

img

ターミナルで Ctrl+C を入力すると Prisma Studio を終了することができます。一旦、Prisma Studio を終了しておいてください。

3.10 DBの初期データの作成

Prisma Studio の画面から1件ずつ手作業でレコードを追加することも可能ですが、ここではプログラムを使って初期データを生成して、それを DB に挿入していきます。なお、データベースの初期データを作成して投入する作業を「シーディング (seeding)」といい、その際に使用する初期データを「シードデータ (Seed Data)」と呼びます。

prisma フォルダのなかに seed.ts というファイルを新規作成して・・・

img

以下のコード (シーディングのためのTypeScriptプログラム) を貼付け、保存してください。

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient(); // PrismaClientのインスタンス生成

const main = async () => {
  // 各テーブルから既存の全レコードを削除
  await prisma.postCategory?.deleteMany();
  await prisma.post?.deleteMany();
  await prisma.category?.deleteMany();

  // カテゴリデータの作成 (テーブルに対するレコードの挿入)
  const c1 = await prisma.category.create({ data: { name: "カテゴリ1" } });
  const c2 = await prisma.category.create({ data: { name: "カテゴリ2" } });
  const c3 = await prisma.category.create({ data: { name: "カテゴリ3" } });

  // 投稿記事データの作成  (テーブルに対するレコードの挿入)
  const p1 = await prisma.post.create({
    data: {
      title: "投稿1",
      content: "投稿1の本文。<br/>投稿1の本文。投稿1の本文。",
      coverImageURL:
        "https://w1980.blob.core.windows.net/pg3/cover-img-red.jpg",
      categories: {
        create: [{ categoryId: c1.id }, { categoryId: c2.id }], // ◀◀ 注目
      },
    },
  });

  const p2 = await prisma.post.create({
    data: {
      title: "投稿2",
      content: "投稿2の本文。<br/>投稿2の本文。投稿2の本文。",
      coverImageURL:
        "https://w1980.blob.core.windows.net/pg3/cover-img-green.jpg",
      categories: {
        create: [{ categoryId: c2.id }, { categoryId: c3.id }], // ◀◀ 注目
      },
    },
  });

  console.log(JSON.stringify(p1, null, 2));
  console.log(JSON.stringify(p2, null, 2));
};

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

上記のプログラム (prisma/seed.ts) は、Next.js からは切り離された独立したプログラムになります。

3.10.1 シーディングコマンドの設定

つづいて package.json を次のように編集してください (第11行目から第13行目の設定を追加してください)。編集する際は、第10行目第13行目の末尾のカンマを忘れないようにしてください。

{
  "name": "next-blog-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^6.6.0",

   以下、省略 

以上の設定により、ts-node --compiler-options {"module":"CommonJS"} prisma/seed.ts というシーディングのための長いコマンドが、npx prisma db seed というショートカットで実行できるようになります。

3.10.2 シーディングの実行と結果の確認

VSCodeのターミナルから次のコマンドを打ち込んで、シーディング (DBの初期データの生成) を実行してください。

npx prisma db seed

なお、seed.ts第41行目第42行目 では、生成した投稿記事の内容を console.log() でログ出力していますが、このログは以下のように (ウェブブラウザの開発者ツールではなく) VSCode のターミナルに出力されることに注意してください。

img

次に、Prisma Studio を起動 (npx prisma studio コマンドを実行) して、DB の各テーブルにレコードが追加されていることを確認してください。

img

Prisma Studio では、日時が ISO8601形式 で表示されることを確認してください。また、第06回講義で解説したように、末尾の ZUTC(協定世界時) を表すことに注意してください。

img

各テーブルのレコードは id カラムの値 (ランダムに設定されたUUIDv4の値) で自動的にソートされることを確認してください。

img

prisma/seed.ts では、中間テーブル (PostCategory) にレコードを追加する文を記述していませんが、実際には当該にテーブルに適切なレコードが追加されていることに注意してください。このような中間テーブルに対するレコードの挿入なども Prisma が提供する ORM としての機能となります。

以上のように、Prisma Client を利用することで、SQL文を記述せずに、TypeScript プログラムから DB に対しての CURD操作 (Create、Update、Read、Delete) が可能になります。

3.10.3 演習 (15分)

どうしても実装できないときはこちらを参照してください。

3.11 スキーマに変更を加えたときは…

テーブルに新しくカラムを追加したり、カラムの名前を変更するなどの スキーマの定義に関わる変更schema.prisma に行ったときは、以下の手続きが必要になります。これらを実行すると、DBのすべてのレコードとテーブルと消える ので注意してください。

  1. ターミナルで npm run devnpx prisma studioを実行中しているときは停止する。
  2. prisma フォルダのなかの dev.db を削除する。
  3. ターミナルから npx prisma db push を実行する。
  4. ターミナルから npx prisma generate を実行する。
    • この処理によって「型」の情報が更新されるので、それが確実に反映されるようにVSCodeの再読み込みを (念のために) 実行する。
  5. ターミナルから npx prisma db seed を実行する。

なお、prisma migrate というコマンドを使用すれば 既存のデータを保持したままスキーマの変更を適用することも可能 ですが、開発の初期段階 (消えて困るようなデータが DB に格納されていない状態) では上記の手続きの方が確実です。

prisma migrate の使い方については公式リファレンスや生成AIなどで調べてください。

以上で、バックエンドで DB を機能させるための準備が完了しました。なお、ここでは、一時的に「SQLite」を使用しますが、最終的には Supabase というクラウドサービスの「PostgreSQL」に切り替えていきます。

4 バックエンド (ウェブAPI) の実装

ここからは、Next.js を使って、ブログアプリ関連の「ウェブAPI」の機能を実装していきます。つまり、前回講義で取り上げた「郵便番号検索API」や「microCMS」のように、特定のエンドポイント (URL) にHTTPリクエストを送ると、JSONフォーマットでデータが取得できる ような機能を Next.js の枠組みで実装していきます。

例えば http://localhost:3000/api/categories というエンドポイントに HTTPリクエスト (GET) を送ると、以下のような HTTP レスポンス (主にJSONデータ) が取得できるような機能を Next.js で実装していきます。

img

4.1 準備

Prisma を利用して RDB とデータをやりとりするためには seed.ts第03行目 のように、PrismaClientクラスのインスタンスを作成する必要があります。そのため、ウェブAPIを実装するプログラムのなかでも const prisma = new PrismaClient(); のような処理が必要となります。

しかし、Next.js を 開発モード (npm run dev) で動かすと、その ホットリロード機能 (=ファイルの変更を検知して自動的にアプリケーションを再起動する機能) との兼ね合いで、Prisma Client インスタンスが重複して生成されるという問題が生じます。このような形で PrismaClientのインスタンス が重複生成されるとメモリリークや「Too many connections」(=データベース接続数が上限を超えた場合に発生するエラー) が生じる可能性があります。ちなみに、開発モード以外では、ホットリロードは使われないのでこの懸念はありません。

この問題を回避するための対策をしていきます。まず src/libprisma.ts というファイルを新規作成してください (libフォルダも存在しないので新規作成してください)。

img

作成した prisma.ts に以下のプログラムを貼付けて保存してください。

import { PrismaClient } from "@prisma/client";

// データベース接続用のインスタンスを作成する関数を定義
const prismaClientSingleton = () => {
  return new PrismaClient();
};

// グローバルスコープに prismaGlobal という変数が存在することをTypeScriptに伝える型定義
declare const globalThis: {
  prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

// 既存の prismaGlobal があればそれを使用し、なければ新しくインスタンスを作成
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();

// 作成した prisma インスタンスを他から利用できるようにエクスポート
export default prisma;

// 開発環境の場合のみ、作成したインスタンスをグローバルスコープに保存し、
// ホットリロード時の再利用を可能にする
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;

このプログラムは、グローバル変数を使って PrismaClientインスタンスを保持し、ホットリロードが起きたときも単一のインスタンスを再利用するように機能します (やや難しい話になるので、現時点で理解できなくても問題ありません)。

詳細については、Next.js の公式リファレンスのBest practice for instantiating Prisma Client with Next.jsを参照してください。また、参考図書の「これからはじめるReact実践入門」の p.592 を参照してください。

4.2 APIの設計

カテゴリに関して次のようにCRUD操作に対応する4個のウェブAPIを実装していきます。カテゴリの新規作成・変更・削除に関係するAPIについては 管理者としてログインしているときだけ利用できる ようにしていきます。

エンドポイント HTTP
Method
処理 管理者
権限
リクエスト
ボディ
/api/categories GET カテゴリの一覧を取得する
/api/admin/categories POST カテゴリを追加する 必要 必要
/api/admin/categories/[id] PUT カテゴリの名前を変更する 必要 必要
/api/admin/categories/[id] DELETE カテゴリを削除する 必要

管理者だけが利用できるように制限する処理は ログイン機能を実装してから追加 します。今回講義のなかでは、誰でも利用可能なAPIとして実装します。

なお、ここでは「カテゴリ」のCRUD操作について解説しますが、次回以降、(宿題で) 投稿記事に関するCRUD操作を各自で実装してもらう予定なので、しっかりと理解しながら読み進めるようにしてください。

4.3 カテゴリの一覧を取得するウェブAPIの実装

Next.js (App Router) では src/app/api の配下に route.ts (≠page.tsx) という名前のファイルを配置することで、ウェブAPIのエンドポイントを作成 することができます。

例えば、以下のように src/app/api/categories/route.ts というファイルを作成して、そこに GET 関数を定義すると http://localhost:3000/api/categories というURLで利用可能なエンドポイントが作成できます(GET Method の HTTPリクエストを受付けて処理できるようになります)。

📂 src/
└─ 📂 app/
    └─ 📂 api/
        └─ 📂 categories/
            └─ route.ts

上記のようにフォルダとファイル (route.ts) を作成して、以下の内容を貼付けて保存してください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";

// [GET] /api/categories カテゴリ一覧の取得
export const GET = async (req: NextRequest) => {
  try {
    const categories = await prisma.category.findMany({
      orderBy: {
        createdAt: "desc", // 降順 (新しい順)
      },
    });
    return NextResponse.json(categories);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの取得に失敗しました" },
      { status: 500 } // 500: Internal Server Error
    );
  }
};

まずは、開発モードでアプリを立ち上げて http://localhost:3000/api/categories にアクセスして、以下のように 「カテゴリの一覧」が JSON形式で得られること を確認してください。なお、Chromeでは、画面上部の「プリティ プリント」にチェックを入れないと下図のように整形表示されないので注意してください。

img

以下、route.ts のコードを解説していきます。

まず、第01行目では、前のセクションで作成した src/app/lib/prisma.ts のなかで定義した PrismaClient (=O/R Mapper) のインスタンスである prisma をインポートしています。基本的は、この prisma を通じて DB とデータのやりとりをするので、このファイルのなかで、あらためて const prisma = new PrismaClient(); のような処理は必要ありません

第07行目 から 第11行目 では、DB のカテゴリテーブルから レコード全件 を取得して、定数 categories に格納しています。ここでは、第01行目 でインポートした prisma が持つ findMany というメソッドを使っていますが、このメソッドは「非同期メソッド」なので、awaitが必要になること に注意してください。ここでは、findMany に対して引数で「createdAt 属性に基づいて降順にソート」するようにも指示しています。戻り値を代入している categories ですが、下図のようにエディタ上でマウスカーソルを重ねると オブジェクトの「配列」 になっていることが確認できます。

img

このように 第07行目categories の型は TypeScript により「型推論」されますが、もし明示的に「型」を指定したい場合は、ファイルの冒頭に import { Category } from "@prisma/client"; を追加したうえで const categories: Category[] = ... のようにします (次の図を参照)。

img

第05行目では、アロー関数形式で GET という関数を定義して処理を記述していますが、ここでの 名前の設定 は非常に重要です。GET という関数名を付けることで「HTTP GET リクエスト」を受付けるようになります。これを GetgetGetCategories などに変更すると機能しなくなるので注意してください。

GET 以外の HTTP Method (POST、PUT、DELETE) を受け付けるウェブAPIを実装するときは、そのメソッドにあわせて関数名を設定する必要があります。HTTP Method の概要が分からないときは、生成AIを使って概要を把握しておいてください。

route.ts のなかに記述する GET / POST / PUT / DELETE 関数は、基本的に NextRequest を引数にして「HTTP Request」の情報を読み取り、NextResponse で「HTTP NextResponse」を生成するように構成します。

(プロンプト例)

Next.js (App Router) でウェブAPIを実装しようとしています。そのための前提知識として HTTP Method について解説してください。

4.3.1 定着確認

HTTPに関する文脈において「リソース」とは URLでアクセス可能なデータや情報、ファイル を指します。

4.4 Thunder Client を使ったAPIのテスト

前回講義のなかで導入した Thunder Client (VSCodeの拡張機能) を使って、前のセクションで実装した APIのエンドポイント /api/categories のテスト (動作検証) を行ないます。予め開発モードでアプリを起動 (npm run dev) しておいてください。

VSCodeの左パネルの「⚡」のアイコンをクリックするか、Ctrl+Shift+R を押下すると Thunder Client のパネルが表示されるので、「New Request」のボタンを押下してください。

img

次に、HTTP Method として「GET」を選択 (確認) して、エンドポイントに http://localhost:3000/api/categories を設定してください。そして、「Send」ボタンを押下してリクエストを送信して、そのレスポンスを確認してください。VSCodeのウィンドウ幅が大きいときは、右側にレスポンスが表示されることがあります。

img

レスポンスでは、ステータスコード (上記の場合は❸の「200 OK」の箇所) が重要となります。基本的には200番台が「成功」、その他、400番台、500番台が「失敗」を表します。知能情報実験実習2の後期のテーマ (サーバ構築基礎実習) のなかでもステータスコードについて習っていると思います。概要を把握しておいてください。

(プロンプト例)

HTTPレスポンスのステータスコードについて、主要なものを解説してください。

4.4.1 定着確認

4.4.2 演習 (5分)

バックエンドで処理される route.ts のなかに console.log() を記述すると、そのログが VSCodeのターミナル に出力されることを確認してください。

VSCode の補完機能により、意図せずに import console from "inspector" などがファイルに追加されていることがあります。それらがインポートされていると console.log() を実行しても VSCodeのターミナルにログが出力されないこと があるので注意してください。console に関して見慣れないインポートがあれば削除してください。

4.4.3 演習 (10分)

PrismaClient の各種メソッドのうち、findMany のようにDBとデータのやりとりをするメソッドは基本的に「非同期処理」になっています。非同期メソッドは await と組み合わせないと期待する動作をしません (awaitの付け忘れは、初心者がハマるポイントです)。

以下のコードのように findMany メソッドに await を付け忘れると、どのようになるか (=コンパイルエラーになるのか、ランタイムエラー (実行時エラー) になるのか、エラーが発生しないのか) を実際に確認してみてください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";

export const GET = async (req: NextRequest) => {
  try {
    const categories = prisma.category.findMany({ // ◀ 意図的にawait忘れ
      orderBy: {
        createdAt: "desc", 
      },
    });
    return NextResponse.json(categories);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの取得に失敗しました" },
      { status: 500 } 
    );
  }
};

コンパイルエラーも、実行時エラーも発生せず、非常に解決しずらいバグ🐛が誕生していること (=何が問題でレスポンスが空になっているか分からない) に注意してください。

一方で、次のように categories明示的に型を与えるとawait を忘れているときに コンパイルエラーが発生して注意喚起してくれること (問題箇所が把握しやすいこと、問題を早期発見できること) を確認してください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client"; // ◀ 型をインポート

export const GET = async (req: NextRequest) => {
  try {
    // ▼ 型を明示している
    const categories: Category[] = prisma.category.findMany({ 
      orderBy: {
        createdAt: "desc",
      },
    });
    return NextResponse.json(categories);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの取得に失敗しました" },
      { status: 500 } 
    );
  }
};

この例のように 「型」を明示するような堅牢なコード を記述することで、結果的として効率的に開発を進めることできます。可読性を損なわない範囲で「型」を明示するようにしてください。

4.4.4 演習 (10分)

次のように意図的にエラーを発生させるコード (第12行目) を追加してください。そのうえで、Thunder Client から HTTPリクエスト を送信し、そのレスポンスがどのようになるか (ステータスコードレスポンスのボディがどのようになるか) を確認してください。

また、VSCodeのターミナルのログも確認してください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

export const GET = async (req: NextRequest) => {
  try {
    const categories: Category[] = await prisma.category.findMany({
      orderBy: {
        createdAt: "desc",
      },
    });
    throw new Error("意図的にエラーを発生させる!!!"); // ◀ 追加
    return NextResponse.json(categories);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの取得に失敗しました" },
      { status: 500 } // 500: Internal Server Error
    );
  }
};

(プロンプト例)

TypeScriptにおける例外処理について解説してください。特に trycatchthrow のキーワードと使い方について解説してください。

4.4.5 演習 (15分)

api/categories/route.ts について、HTTPリクエストを正常に処理できた場合の「戻り値 (=NextResponse)」を、次のように変更したとき、レスポンスの「ステータスコード」と「レスポンスのボディ」がどのように変化するか (あるいは変化しないのか) を Thunder Client から確認してください。

return NextResponse.json(categories);

また、各変更に「どのような意味があるか (あるいは意味がないか)」 についても、生成AIを利用して考察してみてください。

(プロンプト例)

Next.js (App Router) の API Routes の戻り値として return NextResponse.json(categories);return NextResponse.json(categories, { status: 200 }); に書き換えることには、どのような意味や意図があると考えられますか。


変更1 : NextResponse.json メソッドの第2引数に「ステータスコード「200」」を明示的に与える。

return NextResponse.json(categories, { status: 200 });

変更2 : NextResponse.json メソッドの 第1引数 を次のように変更する。

return NextResponse.json({ contents: categories });
return NextResponse.json({
  isSuccess: true,
  contents: categories,
});

4.4.6 EX演習 (宿題: 20分)

prisma.category.findMany第1引数 に与えるオブジェクトを次のように変更すると、categories に格納されるデータがどのように変わるかを確認してください (ThunderClientでレスポンスのボディを確認してください)。

なお、ここでは categories: Category[] = ... のように型を指定するとコンパイルエラーになるので注意してください。

const categories = await prisma.category.findMany({
  select: {
    id: true,
    name: true,
  },
  orderBy: {
    createdAt: "desc",
  },
});
const categories = await prisma.category.findMany({
  select: {
    id: true,
    name: true,
    posts: {
      select: {
        post: {
          select: {
            id: true,
            title: true,
          },
        },
      },
    },
  },
  orderBy: {
    createdAt: "desc",
  },
});
const categories = await prisma.category.findMany({
  select: {
    id: true,
    name: true,
    _count: {
      select: {
        posts: true,
      },
    },
  },
  orderBy: {
    createdAt: "desc",
  },
});

(プロンプト例)

Next.js において Prisma を使ってバックエンド開発をしています。schema.prisma は次のようになっています。
~ (ここに schema.prisma を貼付け) ~
次のように PrismaClient の findMany メソッドを実行すると、categories にはどのような値が得られますか。特に、メソッド引数の select_count の意味について解説してください。

演習を完了したら…

演習に取り組んだ後は “/api/categories”レスポンスボディが以下の形式になるように、一旦、戻しておいてください。次回講義では、この形式のレスポンスであることを前提にフロントエンドを実装します。

[
  {
    "id": "db450dc7-a73b-4311-8b70-3af652efd144",
    "name": "カテゴリ4",
    "createdAt": "2024-12-08T12:00:42.713Z",
    "updatedAt": "2024-12-08T12:00:42.713Z"
  },
  {
    "id": "412b3199-5ae1-45eb-a224-8f53ba3790d0",
    "name": "カテゴリ3",
    "createdAt": "2024-12-08T12:00:42.707Z",
    "updatedAt": "2024-12-08T12:00:42.707Z"
  },
  // ...略...
  {
    "id": "ad28ee69-359f-4d66-aaea-1680c97347b8",
    "name": "カテゴリ1",
    "createdAt": "2024-12-08T12:00:42.695Z",
    "updatedAt": "2024-12-08T12:00:42.695Z"
  }
]

カスタマイズしたコードは削除せずに コメントアウトして残しておくことをお勧めします。あとの授業のなかで、ブログアプリをカスタマイズ・拡張してもらう課題の出題があります。

4.5 カテゴリを追加するウェブAPIの実装

カテゴリを追加 (新規作成) するウェブAPIを実装します。

このAPIは、将来的に (次々回以降の講義で) 管理者でログインしている場合のみ受理される ように拡張していくので、エンドポイントを /api/admin/categories のように設計したいと思います。また、リソースの追加処理に相当するので HTTP Method の POST に対応させるように実装します。

次の位置に route.ts を新規作成してください。

📂 src/
└─ 📂 app/
    └─ 📂 api/
        ├─ 📂 categories/
        │   └─ route.ts ( GET 作成済み )
        └─ 📂 admin/
            └─ 📂 categories/
                └─ route.ts ◀◀◀ 新規作成

次のプログラムを route.ts に貼り付けてください。コードの解説は後回しにします。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

type RequestBody = {
  name: string;
};

export const POST = async (req: NextRequest) => {
  try {
    const { name }: RequestBody = await req.json();
    const category: Category = await prisma.category.create({
      data: {
        name,
      },
    });
    return NextResponse.json(category);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの作成に失敗しました" },
      { status: 500 }
    );
  }
};

開発モード (npm run dev) でアプリを起動して、以下のように Thunder Client からAPIをテストしてください。この APIは、リソース (カテゴリ) を追加をするもの なので、HTTP Method を「POST」に切り替えていることに注意してください。また、エンドポイントのパスに admin を含めている点にも注意してください。

さらに、HTTPリクエストのボディ (Body)に、新規作成するカテゴリの 名前 (name) をJSON形式で与えていることに注目してください。

img

なお、既存のカテゴリの名前 (=例えば、シーディングで挿入した「カテゴリ1」など) をボディに設定してリクエストを送信すると Unique constraint failed on the fields: (name) というエラーで失敗します (その原理で、同じリクエストを2回送信したときにも失敗します)。これは schema.prisma第31行目@unique (ユニーク制約 (値の重複を許可しない制約)) を与えているためです。

(プロンプト例)

Prisma の schema.prismaname String @unique のようにカラムを設定しています。このカラムに適用される制約について解説してください。

つづいて、Prisma Studio を起動して (npx prisma studioを実行して) カテゴリテーブルにレコードが追加されていることを確認してください。あるいは /api/categoriesGETリクエスト を送って、そのレスポンスから カテゴリが正常に追加されていること を確認してください。

img

(既に解説したように、Prisma Studio では、レコードは id でソートされていることに注意してください。追加したカテゴリが末尾に位置するわけではありません)

VSCode: ターミナルの追加と切り替え

VSCodeでは、下図のように「」ボタンを押下するとターミナル (セッション) を追加することができます。そして、1個目のターミナルで rpm run dev を実行して、2個目のターミナルで npx prisma studio を実行するような使い方ができます。

img

4.5.1 演習

{
  "title": "カテゴリ9"
}
{
  "age": 20,
  "name": "カテゴリ9",
}

4.6 カテゴリを追加するウェブAPIの実装 (コードの解説)

解説のためにコードを再掲します。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

type RequestBody = {
  name: string;
};

export const POST = async (req: NextRequest) => {
  try {
    const { name }: RequestBody = await req.json();
    const category: Category = await prisma.category.create({
      data: {
        name,
      },
    });
    return NextResponse.json(category);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの作成に失敗しました" },
      { status: 500 }
    );
  }
};

第05行目 から 第07行目 で、HTTPリクエストの ボディ として受け取るオブジェクトの型を RequestBody として定義しています。

そして、第11行目 で、実際にリクエストのボディから name の値を取得しています。この取得に際しては、TypeScript (JavaScript) の 分割代入 というテクニックが使われています (分割代入については生成AIを利用して理解しておいてください)。第11行目 の処理を分割代入を使わずに分解して書けば、以下のようになります。

const requestBody : RequestBody = await req.json();
const name = requestBody.name;

なお、req.json メソッドも非同期なので await を忘れないようにしてください。

(プロンプト例)

TypeScript (JavaScript) における const { id, name } = user のような「分割代入」について解説してください。

TypeScript (JavaScript) の「分割代入」には、どのようなメリットとデメリットがありますか。

第12行目 から 第15行目 では、PrismaClient の create というメソッドを使ってカテゴリを追加しています。create メソッドでは、実際に追加されたレコード (単体のカテゴリ) が戻り値となります (厳密には、create メソッドの戻り値を await した戻り値がレコードになります)。そして、その値をそのまま 第17行目 でHTTPレスポンスのボディに設定しています。

Thunder Client: リクエストには名前を付けておきましょう

面倒かもしれませんが、各HTTPリクエストに適切に名前をつけておくと、結果的に、長期視点では開発の時短につながります。

img

4.7 カテゴリを削除するウェブAPIの実装

api/admin/categories/[id] のようなエンドポイントに、カテゴリを削除するAPI を実装します。このAPIも管理者だけが実行できるように拡張する予定なので、次のように admin を含むパスに配置します。

📂 src/
└─ 📂 app/
    └─ 📂 api/
        ├─ 📂 categories/
        │   └─ route.ts ( GET 作成済み )
        └─ 📂 admin/
            └─ 📂 categories/
                ├─ route.ts ( POST 作成済み )
                └─ 📂 [id]/
                    └─ route.ts ◀ 新規作成

前回講義で解説したように、Next.js において [id] のような角括弧で囲んだ表記は パラメータプレースホルダ といい、そこには動的に値が入ること意味します。例えば、削除したいカテゴリの id24f932b8-....-569f07ba16a7 の場合、具体的なエンドポイントは次のようになります。

route.ts に次のプログラムを貼付けて保存してください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

type RouteParams = {
  params: {
    id: string;
  };
};

export const DELETE = async (req: NextRequest, routeParams: RouteParams) => {
  try {
    const id = routeParams.params.id;
    const category: Category = await prisma.category.delete({ where: { id } });
    return NextResponse.json({ msg: `「${category.name}」を削除しました。` });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの削除に失敗しました" },
      { status: 500 }
    );
  }
};

URLパスのパラメータプレースホルダ [id] で与えられる値は、第05行RouteParams の型定義、第11行目DELETE 関数定義の「第2引数」、第13行目id = routeParams.params.id によって取得できます。例えば、/api/admin/categories/aaa というエンドポイントにリクエストがあれば、第13行目id には "aaa" という文字列が代入されます。

発展: プレースホルダーパラメータ取得の定石

ここでは、RouteParams という型を定義してプレースホルダーパラメータ (id) を取得しましたが、以下のコードのように インライン型注釈 (Inline Type Annotation) と 分割代入 を利用して型を定義せず、直接的に id を取得することも可能です。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

export const DELETE = async (
  req: NextRequest,
  { params: { id } }: { params: { id: string } } // インライン型注釈・分割代入
) => {
  try {
    const category: Category = await prisma.category.delete({ where: { id } });
// 以下、省略

一般には上記のような表記がよく使用されるので、最初は型定義を使う方法で理解を深め、慣れてきたらインライン型注釈と分割代入を利用する方法に移行することも検討してください。

(プロンプト例)

TypeScript の インライン型注釈 (Inline Type Annotation) について、関数の引数 (オブジェクト型) に適用する例で解説してください。また、type キーワードで「型」を定義せず、インライン型注釈を使うメリットとデメリットは何ですか?

削除処理は 第14行目 にある prisma.category.delete() メソッドにより実行されます。このメソッドは、指定された条件に一致する「単一のレコード」を削除する機能 を持っています。

deleteメソッドに対する条件の与え方

deleteメソッドには、引数を使って「削除対象を指定する条件」を与えます。例えば、"c-1234" という id のレコード (カテゴリ) を削除したいときは、次のように引数を指定します。

prisma.category.delete({ where: { id: "c-1234" } });

この処理は、id という変数を使って次のようにも書けます。

const id = "c-1234";
prisma.category.delete({ where: { id: id } });

TypeScript (JavaScript) では、オブジェクトの「プロパティ名」と「値の変数名」が同じ場合、省略記法を使うことができます。つまり、id: id を、単に id と書くことができます (省表記が利用できます)。これを適用すれば、先のコードは、次のように記述することができます。

const id = "c-1234";
prisma.category.delete({ where: { id } });

慣れるまでは非常に分かりづらいと思いますが、実務では頻繁に利用される記法なので覚えておいてください。第14行目 は、この略表記を使って記述されています。

なお、このような記法を オブジェクトリテラルのプロパティ省略記法 (Property Shorthand) といいます。

なお、delete メソッドは削除に成功したとき、当該のレコードを戻り値とします。第15行目 では、その戻り値を利用して、「カテゴリ5」を削除しました。 のような HTTP レスポンスを返すようにしています。


ファイルの編集が完了したら開発モードでアプリを起動して、Thunder Client からテストを実行してください。HTTP Method を「DELETE」に切り替え、ボディは削除してください (ボディを残していても支障はありません)。

エンドポイントの [id] の部分 (=削除したいカテゴリのID) については、Prisma Studio から調べて設定してください。

img

存在しない id をエンドポイントに指定した場合、どのようなレスポンスとなるか確認しておいてください。

4.8 カテゴリを編集するウェブAPIの実装

カテゴリの 名前を変更 (更新) するためのウェブAPI を実装します。具体的には、次のようなエンドポイントに対して、新しい名前の情報を持ったボディを「PUTリクエスト」で送信することで、これを実現します。

既に気づいたかもしれませんが、このエンドポイントは DELETEメソッドと共通 になります。route.ts には複数のメソッドを含めることが可能なので api/admin/categories/[id]/route.ts を次のように編集してください。

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@prisma/client";

type RouteParams = {
  params: {
    id: string;
  };
};

// ▼▼▼ 追加: ここから ▼▼▼
type RequestBody = {
  name: string;
};

export const PUT = async (req: NextRequest, routeParams: RouteParams) => {
  try {
    const id = routeParams.params.id;
    const { name }: RequestBody = await req.json();
    const category: Category = await prisma.category.update({
      where: { id },
      data: { name },
    });
    return NextResponse.json(category);
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの名前変更に失敗しました" },
      { status: 500 }
    );
  }
};
// ▲▲▲ 追加: ここまで ▲▲▲

export const DELETE = async (req: NextRequest, routeParams: RouteParams) => {
  try {
    const id = routeParams.params.id;
    const category: Category = await prisma.category.delete({ where: { id } });
    return NextResponse.json({ msg: `「${category.name}」を削除しました。` });
  } catch (error) {
    console.error(error);
    return NextResponse.json(
      { error: "カテゴリの削除に失敗しました" },
      { status: 500 }
    );
  }
};

第20行目update メソッドでは、引数に与えるオブジェクトの where プロパティで「更新対象のレコードを指定」し、data プロパティで「更新する値 (上書きする値)」を指定しています。

ここでも、先と同じく {id: id}{ id } のように略表記するテクニック、{ name: name }{ name } のように略表記するテクニックが使用されています。

ファイルの編集が完了したら開発モードでアプリを起動して、Thunder Client でテストしてください。

img

5 次回の授業

次回の授業では、投稿記事関連のウェブAPIの設計と実装をおこないます。また、フロントエンドから、それらAPIを利用する画面の実装も行ないます。