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

2025年12月11日(木)5・6時限

1 連絡・概要

1.1 連絡

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

1.2 案内

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

リポジトリの README について、次のチェックリストに「3個以上該当する」と、就活 (インターン) やコンテストの応募では「アプリを起動してもらえない」または「コードを見てもらえない」ということになりがちです。

アピールが弱いREADME、やっつけ仕事のREADME生成AIで作成した雰囲気の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 でターミナルを開き、以下のコマンドを実行してください (1行づつ実行してください)。

npm i -D prisma 
npm i @prisma/adapter-better-sqlite3 @prisma/client

(プロンプト例)

Next.js において、Prisma (v7) を ORM として用いて RDB (SQLite3) を利用しようと考えています。このためには prisma@prisma/adapter-better-sqlite3@prisma/client というライブラリが必要なことが分かったのですが、それぞれの役割の違いについて教えてください。また、-D オプションをつけてインストールすべきライブラリはどれですか。Prisma のバージョンが最新の v7 であることに注意して回答してください。

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

npm i -D tsx

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 vulnerabilities (Y moderate, Z critical)」のように表示されたときは 既存のライブラリにセキュリティ脆弱性 が検出されたことを意味します。開発環境としてインストール済みのパッケージのなかに脆弱性を持つものが存在し、それに対してアップデート対応を促すメッセージとなっています。

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

npm audit

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

npm audit fix

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

npm audit fix --force

なお、npm audit fix --force (=破壊的変更を含む強制アップデート) の実行後は、npm run build コマンドで、プロジェクトが正常にビルドできることを確認してください。

(プロンプト例)

脆弱性が見つかったため、ライブラリをアップデートするように指示されました。npm audit fixnpm audit fix --force の違いについて教えてください。

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

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

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

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

npx prisma init --datasource-provider sqlite

このコマンドを実行すると、

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 prisma.config.ts の編集

prisma.config.ts は、Prisma 全体の挙動を制御する設定ファイル となります。

以下のように、シーディング (seeding) に関する設定を seed: "tsx prisma/seed.ts", を追記してください。シーディングとは、データベースに初期データを投入するための処理を指します。

// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts", // 追記
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

第10行目 では、シーディングコマンド (npx prisma db seed) を実行したときに参照されるファイル (prisma/seed.ts) を指定しています。現時点では、まだ prisma/seed.ts は存在しません。

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

3.8 schema.prisma の確認

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

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "sqlite"
}

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

第03行目 では、次のセクションで schema.prisma に追記する「DBのスキーマ定義」から自動生成する 型情報などを出力するフォルダ を指定しています。

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

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

3.9 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"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "sqlite"
}

// 投稿記事テーブル
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.10 データベースの作成と確認

schema.prisma を編集・保存したら、次のコマンドを実行してください (少し時間がかかりす)。

npx prisma db push

正常終了すると「Your database is now in sync with your Prisma schema.」のような応答があります。このコマンドにより、schema.prisma に基づいて 実際にデータベースが新規作成され (現在の設定の場合は dev.db というファイルがプロジェクトフォルダのルートに新規作成され)、その DB のなかに 「3つのテーブル」が作成 されます。

既にデータベースが存在する場合は The database is already in sync with the Prisma schema. となります。

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

npx prisma generate

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

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

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

img

つづいて、VSCode から、次のようにプロジェクトのルートに dev.db (SQLiteのDBファイル) が作成されていることを確認してください。

img

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

npx prisma studio

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

img

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

img

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

3.11 DBの初期データ投入の準備 (src/lib/prisma.ts の新規作成)

Prisma を使って DB を操作するとき (たとえば、初期データの投入するとき) は、PrismaClient クラスからインスタンスを生成し、それを利用します。しかし、DB 操作の処理毎に PrismaClient のインスタンスを生成すると、DB 接続数が無駄に増えてリソースを圧迫するという問題 が生じます。

くわえて、Next.js を開発モード (npm run dev で起動) では、ホットリロード (=ファイルの変更を検知して自動的にアプリケーションを再実行する機能) のたびに PrismaClient が重複生成されて接続数が増えていくという問題があります。

そこで、アプリ全体で 1 つだけ PrismaClient インスタンスを共有する仕組み が必要になります。以下の src/lib/prisma.ts は、そのための実装になります。

プロジェクトフォルダに src/lib/prisma.ts というファイルを新規作成して、以下の内容を記述してください。

import { PrismaClient } from "@/generated/prisma/client";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";

const globalForPrisma = global as unknown as {
  prisma: PrismaClient;
};

const adapter = new PrismaBetterSqlite3({
  url: process.env.DATABASE_URL || "file:./dev.db",
});

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    adapter,
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

3.12 DBの初期データの作成

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

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

img

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

import { prisma } from "@/lib/prisma";

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.12.1 シーディングコマンドの実行と結果の確認

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

npx prisma db seed

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

img

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

img

Prisma Studio では、日時が ISO8601形式 で表示されることを確認してください。末尾の +00:00UTC(協定世界時) を表すことに注意してください。

img

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

img

このような中間テーブルに対するレコードの挿入なども Prisma が提供する ORM の機能となります。

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

3.12.2 演習 (15分)

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

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

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

  1. ターミナルで npm run devnpx prisma studioを実行中しているときは停止する。
  2. プロジェクトルートの 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 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.2 カテゴリの一覧を取得するウェブ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 とデータのやりとりをします。

第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.2.1 定着確認

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

4.3 httpYac を使ったAPIのテスト

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

devtools/httpyac/LocalApi.http というファイルを新規作成して、以下のように HTTP Method として「GET」により、エンドポイントに http://localhost:3000/api/categories にリクエストを送るためのコードを記述してください。

# [Ctrl]+[Alt]+[R] で各セクションを実行

### カテゴリ一覧の取得
GET http://localhost:3000/api/categories

つづいて、当該セクションをアクティブにして [Ctrl]+[Alt]+[R] を押下するか、send を押下してリクエストを実行してください。

img

成功すると、次のような応答が返ってきます (あらかじめ npx run dev で開発モードでアプリを起動しておく必要があります)。

img

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

(プロンプト例)

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

4.3.1 定着確認

4.3.2 演習 (5分)

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

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

4.3.3 演習 (10分)

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

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

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

// [GET] /api/categories カテゴリ一覧の取得
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 "@/generated/prisma/client"; // ◀ 型をインポート

// [GET] /api/categories カテゴリ一覧の取得
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 }, // 500: Internal Server Error
    );
  }
};

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

4.3.4 演習 (10分)

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

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

import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/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.3.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.3.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.4 カテゴリを追加するウェブ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 "@/generated/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) でアプリを起動して、以下のように httpYac からAPIをテストしてください。この APIは、リソース (カテゴリ) を追加をするもの なので、HTTP Method を「POST」にしていることに注意してください。また、エンドポイントのパスに admin を含めている点にも注意してください。

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

# [Ctrl]+[Alt]+[R] で各セクションを実行

### カテゴリ一覧の取得
GET http://localhost:3000/api/categories

### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json

{
  "name": "カテゴリ5"
}

なお、既存のカテゴリの名前 (=例えば、シーディングで挿入した「カテゴリ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

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

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

img

4.4.1 演習

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

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

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

import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/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レスポンスのボディに設定しています。

4.6 カテゴリを削除するウェブ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 "@/generated/prisma/client";

type RouteParams = {
  params: Promise<{
    id: string;
  }>;
};

export const DELETE = async (req: NextRequest, routeParams: RouteParams) => {
  try {
    const { id } = await routeParams.params;
    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行目const { id } = await routeParams.params によって取得できます。例えば、/api/admin/categories/aaa というエンドポイントにリクエストがあれば、第13行目id には "aaa" という文字列が代入されます。

削除処理は 第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 レスポンスを返すようにしています。


ファイルの編集が完了したら開発モードでアプリを起動して、httpYac からテストを実行してください。HTTP Method を「DELETE」とします。

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

# [Ctrl]+[Alt]+[R] で各セクションを実行

### カテゴリ一覧の取得
GET http://localhost:3000/api/categories

### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json

{
  "name": "カテゴリ5"
}

### カテゴリの削除
@post_id = 46562fea-edf7-4d72-9ac3-8add07b38260
DELETE http://localhost:3000/api/admin/categories/{{post_id}}

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

4.7 カテゴリを編集するウェブ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 "@/generated/prisma/client";

type RouteParams = {
  params: Promise<{
    id: string;
  }>;
};

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

export const PUT = async (req: NextRequest, routeParams: RouteParams) => {
  try {
    const { id } = await routeParams.params;
    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 } = await routeParams.params;
    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 } のように略表記するテクニックが使用されています。

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

# [Ctrl]+[Alt]+[R] で各セクションを実行

### カテゴリ一覧の取得
GET http://localhost:3000/api/categories

### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json

{
  "name": "カテゴリ6"
}

### カテゴリの削除
@post_id = 46562fea-edf7-4d72-9ac3-8add07b38260
DELETE http://localhost:3000/api/admin/categories/{{post_id}}

### カテゴリの名前を変更
@post_id = 05400979-05c1-4c14-a477-6ed54013a963
PUT http://localhost:3000/api/admin/categories/{{post_id}}
Content-Type: application/json

{
  "name": "カテゴリA"
}

5 次回の授業

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