【個人開発_#5】APIの開発

前回は、SupabaseとPrismaを導入してデータの永続化（保存）を実装しました。
今回はAPIの開発周りです。
（実は前回すでに実装しており、Supabaseからデータを取得するところはできていました。今回は、もう少し複雑なAPIの部分になります。）

## テーブルとSchemaの定義
今回定義したテーブルは次の通りになります。

```mermaid
erDiagram
    User ||--o{ Favorite : "favorites"
    User ||--o{ Favorite : "favoritedBy"
    User ||--o| Address : "currentAddress"
    User ||--o| DeploymentLocation : "deploymentLocation"
    User ||--o| Cohort : "cohort"
    Address ||--|| Country : "country"
    Address ||--o| Prefecture : "prefecture"
    DeploymentLocation ||--|| Country : "country"
    Prefecture ||--|| Country : "country"

    User {
        int id PK
        string name
        datetime createdAt
        string bio
        string image
        string currentAddressId FK
        string deploymentLocationId FK
        string cohortId FK
        string twitterUrl
        string instagramUrl
        string websiteUrl
    }

    Address {
        string id PK
        string countryId FK
        string prefectureId FK
        string city
        string otherAddressDetails
    }

    Country {
        string id PK
        string name
        boolean isDeveloping
    }

    Prefecture {
        string id PK
        string name
        string countryId FK
    }

    DeploymentLocation {
        string id PK
        string countryId FK
        string specificLocation
    }

    Cohort {
        string id PK
        string cohortYear
        string cohortTerm
    }

    Favorite {
        string id PK
        int userId FK
        int favoriteUserId FK
    }
```

そもそもテーブル設計などやったことがない僕はこれが良い設計なのか正直わかりませんが、ChatGPTくんに相談しながらこの形になりました。
プロフィール情報に「現住所」と「派遣国」が入るため`Country`テーブルを作成して`Address`テーブルと`DeploymentLocation`で使いまわせるようにしました。

次に、Schemaを定義します。前回は仮でSchemaを定義しただけだったので今回は上記のテーブルに合わせて定義します。
```typescript: schema.prisma
generator client {
  provider = "prisma-client-js"
}

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

model User {
  id                   Int       @id @default(autoincrement())
  name                 String
  createdAt            DateTime  @default(now())
  image                String?
  currentAddressId     String?
  bio                  String?
  deploymentLocationId String?
  cohortId             String?
  twitterUrl           String?
  instagramUrl         String?
  websiteUrl           String?
  currentAddress       Address?  @relation("CurrentAddress", fields: [currentAddressId], references: [id])
  deploymentLocation   DeploymentLocation? @relation(fields: [deploymentLocationId], references: [id])
  cohort               Cohort?   @relation(fields: [cohortId], references: [id])
  favorites            Favorite[] @relation("UserFavorites")
  favoritedBy          Favorite[] @relation("FavoritedUsers")
}

model Address {
  id                   String   @id @default(uuid())
  countryId            String
  prefectureId         String?
  city                 String?
  otherAddressDetails  String?
  country              Country  @relation(fields: [countryId], references: [id])
  prefecture           Prefecture? @relation(fields: [prefectureId], references: [id])
  users                User[]   @relation("CurrentAddress")
}

model Country {
  id                   String   @id @default(uuid())
  name                 String   @unique
  isDeveloping         Boolean  @default(false)
  addresses            Address[]
  deploymentLocations  DeploymentLocation[]
  prefectures          Prefecture[]
}

model Prefecture {
  id                   String   @id @default(uuid())
  name                 String
  countryId            String
  country              Country  @relation(fields: [countryId], references: [id])
  addresses            Address[]
}

model DeploymentLocation {
  id                   String   @id @default(uuid())
  countryId            String
  specificLocation     String
  country              Country  @relation(fields: [countryId], references: [id])
  users                User[]
}

model Cohort {
  id                   String   @id @default(uuid())
  cohortYear           String
  cohortTerm           String
  users                User[]
}

model Favorite {
  id                   String   @id @default(uuid())
  userId               Int
  favoriteUserId       Int
  user                 User     @relation("UserFavorites", fields: [userId], references: [id])
  favoriteUser         User     @relation("FavoritedUsers", fields: [favoriteUserId], references: [id])

  @@unique([userId, favoriteUserId])
}
```

### Models
データモデルの定義を行います。
```typescript
model User {
  // 省略
}
```

https://www.prisma.io/docs/orm/prisma-schema/data-model/models

### Relations
2つのモデルのリレーションを張ります。
UserFavorites
```typescript
model User {
  favorites            Favorite[] @relation("UserFavorites")
}
```
`favorites`フィールドは、`Favorite`モデルとの1対多の関係を定義しています。
`@relation("UserFavorites")`は、この関係に`"UserFavorites"`という名前を付けています。

```typescript
model Favorite {
  id                   String   @id @default(uuid())
  userId               Int
  favoriteUserId       Int
  user                 User     @relation("UserFavorites", fields: [userId], references: [id])
  favoriteUser         User     @relation("FavoritedUsers", fields: [favoriteUserId], references: [id])

  @@unique([userId, favoriteUserId])
}
```
Favoriteモデルでは、あるユーザーが別のユーザーをお気に入りに追加した関係を表します。
- `id`: ユニークな識別子で、UUIDが自動生成されます。
- `userId`: お気に入りを追加したユーザーのID。
- `favoriteUserId`: お気に入りに追加されたユーザーのID。
- `user`:  `userId`を通じて`User`モデルと関連付けられています（お気に入りを追加したユーザー）。
- `favoriteUser`: `favoriteUserId`を通じて`User`モデルと関連付けられています（お気に入りに追加されたユーザー）。
- `@@unique([userId, favoriteUserId])`: 同じユーザーペアの重複を防ぐためのユニーク制約。

## プロフィールの一覧ページ
`/allusers/`にアクセスすると一覧ページが見えるようにしようと思います。
前回は実装だと...
```app/api/users/route.ts
import { PrismaClient } from '@prisma/client';
import { NextResponse } from 'next/server';

const prisma = new PrismaClient();

export const GET = async () => {
  try {
    await prisma.$connect();
    const users = await prisma.user.findMany();
    return NextResponse.json(users, { status: 200 });
  } catch (err) {
    return NextResponse.json({ message: 'Server error', err }, { status: 500 });
  } finally {
    await prisma.$disconnect();
  }
};
```
`/api/users/`を叩くと下記のようなデータが返ってきます。
```json
[
  {
    "id": 1,
    "name": "鈴木花子",
    "createdAt": "2024-09-21T03:34:41.295Z",
    "image": "sample-person.jpg",
    "currentAddressId": "JP002",
    "deploymentLocationId": "DL002",
    "cohortId": "C2020-2",
    "twitterUrl": "https://twitter.com/suzukihanako",
    "instagramUrl": "https://instagram.com/suzukihanako",
    "websiteUrl": "https://suzukihanako.com"
  },
  {
    ...
  },
  {
    ...
  },
]
```
プロフィールに現住所や派遣国を表示することができないことに気づきました。
なのでAPIを改修する必要があり下記のように対応しました。
```typescript
const users = await prisma.user.findMany({
  include: {
    currentAddress: {
      include: {
        prefecture: true,
        country: true,
      },
    },
    deploymentLocation: {
      include: {
        country: true,
      },
    },
    cohort: true,
  },
});
```
`include`すると必要なテーブルのデータを取得することができます。
```json
[
  {
    "id": 2,
    "name": "鈴木花子",
    "createdAt": "2024-09-21T03:34:41.295Z",
    "image": "sample-person.jpg",
    "currentAddressId": "JP002",
    "deploymentLocationId": "DL002",
    "cohortId": "C2020-2",
    "twitterUrl": "https://twitter.com/suzukihanako",
    "instagramUrl": "https://instagram.com/suzukihanako",
    "websiteUrl": "https://suzukihanako.com",
    
    "currentAddress": {
      "id": "JP002",
      "countryId": "85",
      "prefectureId": "14",
      "city": "横浜市",
      "otherAddressDetails": null,
      "prefecture": {
        "id": "14",
        "name": "神奈川県",
        "countryId": "1"
      },
      "country": {
        "id": "85",
        "name": "日本",
        "isDeveloping": false
      }
    },
    
    "deploymentLocation": {
      "id": "DL002",
      "countryId": "78",
      "specificLocation": "ジャカルタ",
      "country": {
        "id": "78",
        "name": "インドネシア",
        "isDeveloping": true
      }
    },
    
    "cohort": {
      "id": "C2020-2",
      "cohortYear": "2020",
      "cohortTerm": "2"
    }
  },
  {
    ...
  },
  {
    ...
  }
]
```
もっと整理できそうな気がするけど、一旦これで進めます...。

```typescript:ProfileCard.ts
import Link from 'next/link';
import { UserType } from '@/app/type';
import Image from 'next/image';

export default function ProfileCard(user: UserType) {
  const { id, name, image, currentAddress } = user;
  return (
    <Link href={`/${id}`} passHref className="mb-4 block rounded-lg bg-white p-6 shadow-md">
      <div className="flex items-center">
        <Image
          width={200}
          height={200}
          src={`http://localhost:3000/images/${image}`}
          alt={name}
          className="mr-4 size-16 rounded-full object-cover"
        />
        <div>
          <h2 className="text-xl font-bold">{name}</h2>
          <p className="text-gray-600">
            {currentAddress.country.name} {currentAddress.prefecture?.name}
          </p>
        </div>
      </div>
      <p className="mt-4 text-gray-800">コメントが入ります</p>
    </Link>
  );
}
```
これで都道府県を表示することができるようになりました。
![CleanShot 2024-09-22 at 07.01.07@2x.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592040/672ef400-6aed-05b3-8ed8-45a41bafe1ca.jpeg)

一旦Prismaを使ってDBからデータを取得して表示させるところはこのへんにしておきます。
他のページのAPIも作っていきます🫶🏿