チャットアプリ作成 NextJS ✖️ API Gateway ✖️ Lambda ✖️ DynamoDB

# 成果物
API GatewayのWebsocket APIを使用してリアルタイムチャットアプリを作成してみました。
![chat.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951236/911c1bf1-c5bb-fd57-410f-277052dc2309.gif)



# 経緯
以前、以下の記事でSocket.IOをNextJSのサーバーサイドに実装しチャットアプリを作成しました。
- https://qiita.com/washizawa83/items/fa19a246120ad9208917

NextJSのサーバーサイドにSocket.IOを実装する際の問題点
- Vercelにデプロイすることができない（Websocketのカスタムサーバーを建てることが出来ない）
- 別途WebsocketサーバーをEC2などに構築する必要がありそう
- EC2にサーバーを構築する場合、料金が高くなる

そこでサーバーレスでWEbsocketを構築する方法であるAPI GateweayのWebsocketを使用してみることにしました。

# 全体のイメージ
![スクリーンショット 2024-12-18 21.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951236/6fa3350c-3d53-af43-9004-d883cb34aaf7.png)

![スクリーンショット 2024-12-18 21.10.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951236/3fd7cb11-7a6c-5cac-3e98-c99c003bcab7.png)

目次
- DynamoDBにテーブル作成
    - roomsテーブル
    - connectionsテーブル
- Lambda関数の作成
    - createRoom関数
    - getAllRooms関数
    - connect関数
    - sendMessage関数
    - disconnect関数
- API Gatewayの作成
    - rest api作成
    - websocket api作成
- フロントエンドの実装


# DynamoDBにテーブル作成
以下構成のテーブルを作成します。
### roomsテーブル
| 属性名 | 説明 |
| :---: | :---: |
| room_id | 部屋の識別子を保持 |
| name | 部屋名を保持 |

### connectionsテーブル
| 属性名 | 説明 |
| :---: | :---: |
| connection_id | 接続情報を保持 |
| joined_room_id | 部屋の識別子を保持 |

## roomsテーブルの作成
部屋情報を保持するテーブルを作成します。
役割：部屋情報の一覧を取得するために使用します。

### 手順
1. AWSマネジメントコンソールからDynamoDBを検索
2. サイドメニューから 「テーブル」 ⇨ 「テーブルを作成」
3. テーブル名・パーティションキーを以下に設定
    - テーブル名：`rooms`
    - パーティションキー：`room_id`
4. 「テーブル作成」を押す

## connectionsテーブルの作成
Websocketの接続IDを保持するテーブルを作成します。
役割：自分の入室している部屋の取得、部屋に入室している参加者の一覧を取得など

1. AWSマネジメントコンソールからDynamoDBを検索
2. サイドメニューから 「テーブル」 ⇨ 「テーブルを作成」
3. テーブル名・パーティションキーを以下に設定
    - テーブル名：`connections`
    - パーティションキー：`connection_id`
4. 「テーブル作成」を押す
5. テーブル一覧からconnectionsテーブルを選択
6. タブから「インデックス」 ⇨ 「インデックスの作成」を選択
7. パーティションキー・インデックス名を以下に設定
    - パーティションキー：`joined_room_id`
    - インデックス名：`joined_room_id-index`
8. 「インデックスの作成」を押す

部屋に入室している参加者の一覧を取得するためにパーティションキーではない`joined_room_id`で検索できるようにしたいためグローバルセカンダリインデックス(GSI)を作成しました。

以上でDynamoDBのテーブル作成は終わりです。

# Lambda関数作成
以下の4つの関数を作成していきます。
- createRoom関数
- getAllRooms関数
- connect関数
- sendMessage関数
- disconnect関数

## createRoom関数作成
DynamoDBに部屋情報を新規作成する関数を作成します。
### 手順
1. AWSマネジメントコンソールからLambdaを検索
2. ダッシュボード右上の「関数の作成」
3. 関数名・ランタイムを以下に設定
    - 関数名：`createRoom`
    - ランタイム：Node.js
4. 「関数の作成」を押す
5. コードに以下「createRoomのコード」を記述し「Deploy」押す
6. 「設定」タブ ⇨ 「アクセス権限」から「ロール名」をクリック
7. 許可ポリシーの「AWSLambdaBasicExecutionRole」をクリック
8. 「編集」で以下の許可ポリシーを追加する
9. 「次へ」 ⇨ 「変更を保存」でポリシーを反映

<details><summary>createRoom関数のコード</summary>

```js
import {
  ApiGatewayManagementApiClient,
  PostToConnectionCommand,
} from "@aws-sdk/client-apigatewaymanagementapi";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  PutCommand,
  DynamoDBDocumentClient,
  GetCommand,
} from "@aws-sdk/lib-dynamodb";
import { randomUUID } from 'crypto';

export const handler = async (event) => {
  const client = new DynamoDBClient({});
  const docClient = DynamoDBDocumentClient.from(client);

  const body = JSON.parse(event.body);
  const roomName = body.roomName
  const roomId = randomUUID()

  // DynamoDBにレコード追加
  const putRoomCommand = new PutCommand({
    TableName: "rooms",
    Item: {
      room_id: roomId,
      name: roomName,
    },
    // 既に同一のroom_idが存在する場合、レコードを追加しない
    ConditionExpression: "attribute_not_exists(room_Id)",
  });

  try {
    await docClient.send(putRoomCommand);

    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
        "Access-Control-Allow-Headers": "Content-Type",
      },
      body: JSON.stringify({
        message: "Room created successfully",
        roomId,
      }),
    };
  } catch (error) {
    return {
      statusCode: 500,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
        "Access-Control-Allow-Headers": "Content-Type",
      },
      body: JSON.stringify({
        message: "Failed to create room",
        error: error.message,
      }),
    };
  }
};
```

</details>


<details><summary>許可ポリシーの追加</summary>

```diff_json
{
    "Statement": {
        {
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
				"dynamodb:*"
			],
			"Resource": "*"
		},
		{
			"Sid": "Statement2",
			"Effect": "Allow",
			"Action": [
				"execute-api:*"
			],
			"Resource": "*"
		}
    }
}
```

</details>

## getAllRooms関数作成
DynamoDBに保持した部屋情報を全て返す関数を作成します。
### 手順
1. AWSマネジメントコンソールからLambdaを検索
2. ダッシュボード右上の「関数の作成」
3. 関数名・ランタイムを以下に設定
    - 関数名：`getAllRooms`
    - ランタイム：Node.js
4. 「関数の作成」を押す
5. コードに以下「getAllRooms関数のコード」を記述し「Deploy」押す
6. 「設定」タブ ⇨ 「アクセス権限」から「ロール名」をクリック
7. 許可ポリシーの「AWSLambdaBasicExecutionRole」をクリック
8. 「編集」で以下の許可ポリシーを追加する
9. 「次へ」 ⇨ 「変更を保存」でポリシーを反映

<details><summary>getAllRooms関数のコード</summary>

```js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";

export const handler = async (event) => {
  const client = new DynamoDBClient({});
  const docClient = DynamoDBDocumentClient.from(client);

  const getAllRoomsCommand = new ScanCommand({
    TableName: "rooms",
  });

  try {
    const allRooms = await docClient.send(getAllRoomsCommand);

    const response = {
      statusCode: 200,
      body: JSON.stringify({
        rooms: allRooms.Items,
      }),
    };
    return response;
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: "Failed to fetch rooms",
        error: error.message,
      }),
    };
  }
};

```
今回はお試しで作成しているのでScanで全件取得していますが、データ量が増えるケースではQueryで取得したほうが料金、パフォーマンス的に嬉しいはずです。

</details>

<details><summary>許可ポリシーの追加</summary>

```diff_json
{
    "Statement": {
        {
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
				"dynamodb:*"
			],
			"Resource": "*"
		},
		{
			"Sid": "Statement2",
			"Effect": "Allow",
			"Action": [
				"execute-api:*"
			],
			"Resource": "*"
		}
    }
}
```

</details>

## connect関数作成
Websocket APIに接続する関数を作成します。
### 手順
1. AWSマネジメントコンソールからLambdaを検索
2. ダッシュボード右上の「関数の作成」
3. 関数名・ランタイムを以下に設定
    - 関数名：`connect`
    - ランタイム：Node.js
4. 「関数の作成」を押す
5. コードに以下「connect関数のコード」を記述し「Deploy」押す
6. 「設定」タブ ⇨ 「アクセス権限」から「ロール名」をクリック
7. 許可ポリシーの「AWSLambdaBasicExecutionRole」をクリック
8. 「編集」で以下の許可ポリシーを追加する
9. 「次へ」 ⇨ 「変更を保存」でポリシーを反映

<details><summary>connect関数のコード</summary>

```js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  PutCommand,
  DynamoDBDocumentClient,
  GetCommand,
} from "@aws-sdk/lib-dynamodb";

export const handler = async (event) => {
  const client = new DynamoDBClient({});
  const docClient = DynamoDBDocumentClient.from(client);

  const roomId = event.queryStringParameters?.roomId
  const connectionId = event.requestContext.connectionId;

  const putConnectionCommand = new PutCommand({
    TableName: "connections",
    Item: {
      connection_id: connectionId,
      joined_room_id: roomId
    },
  });

  await docClient.send(putConnectionCommand);

  return {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
      "Access-Control-Allow-Headers": "Content-Type",
    },
  };
};
```

</details>

<details><summary>許可ポリシーの追加</summary>

```diff_json
{
    "Statement": {
        {
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
				"dynamodb:*"
			],
			"Resource": "*"
		},
		{
			"Sid": "Statement2",
			"Effect": "Allow",
			"Action": [
				"execute-api:*"
			],
			"Resource": "*"
		}
    }
}
```

</details>

## sendMessage関数作成
部屋の参加者全員にメッセージを送信する関数。
### 手順
1. AWSマネジメントコンソールからLambdaを検索
2. ダッシュボード右上の「関数の作成」
3. 関数名・ランタイムを以下に設定
    - 関数名：`sendMessage`
    - ランタイム：Node.js
4. 「関数の作成」を押す
5. コードに以下「sendMessage関数のコード」を記述し「Deploy」押す
6. 「設定」タブ ⇨ 「アクセス権限」から「ロール名」をクリック
7. 許可ポリシーの「AWSLambdaBasicExecutionRole」をクリック
8. 「編集」で以下の許可ポリシーを追加する
9. 「次へ」 ⇨ 「変更を保存」でポリシーを反映

<details><summary>sendMessage関数のコード</summary>

```js
import {
  ApiGatewayManagementApiClient,
  PostToConnectionCommand,
} from "@aws-sdk/client-apigatewaymanagementapi";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  PutCommand,
  DynamoDBDocumentClient,
  GetCommand,
  QueryCommand
} from "@aws-sdk/lib-dynamodb";

export const handler = async (event) => {
  const client = new DynamoDBClient({});
  const docClient = DynamoDBDocumentClient.from(client);
  const connectionId = event.requestContext.connectionId;

  const getConnectionIdCommand = new GetCommand({
    TableName: "connections",
    Key: {
      connection_id: connectionId,
    },
  });
  const connection = await docClient.send(getConnectionIdCommand);
  const roomId = connection.Item.joined_room_id;

  const getJoinedRoomConnectionIdsCommand = new QueryCommand({
    TableName: "connections",
    IndexName: "joined_room_id-index",
    KeyConditionExpression: "joined_room_id = :roomId",
    ExpressionAttributeValues: {
      ":roomId": roomId,
    },
  });

  const joinedRoomConnectionIdsResponse = await docClient.send(getJoinedRoomConnectionIdsCommand);
  const joinedRoomConnectionIds = joinedRoomConnectionIdsResponse.Items

  const body = JSON.parse(event.body);
  const message = {
    type: "message",
    message: body.data.message
  }

  const apigClient = new ApiGatewayManagementApiClient({
    endpoint:
      "https://" +
      event.requestContext.domainName +
      "/" +
      event.requestContext.stage,
  });

  const sendMessages = joinedRoomConnectionIds.map(async (connectionId) => {
    const apigCommand = new PostToConnectionCommand({
      ConnectionId: connectionId.connection_id,
      Data: JSON.stringify({ message }),
    });

    await apigClient.send(apigCommand);
  });

  await Promise.all(sendMessages);

  return {
    statusCode: 200,
  };
};
```

</details>

<details><summary>許可ポリシーの追加</summary>

```diff_json
{
    "Statement": {
        {
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
				"dynamodb:*"
			],
			"Resource": "*"
		},
		{
			"Sid": "Statement2",
			"Effect": "Allow",
			"Action": [
				"execute-api:*"
			],
			"Resource": "*"
		}
    }
}
```

</details>

## disconnect関数作成
Websocket APIの接続が切れた時に発火する関数。
### 手順
1. AWSマネジメントコンソールからLambdaを検索
2. ダッシュボード右上の「関数の作成」
3. 関数名・ランタイムを以下に設定
    - 関数名：`disconnect`
    - ランタイム：Node.js
4. 「関数の作成」を押す
5. コードに以下「disconnect関数のコード」を記述し「Deploy」押す
6. 「設定」タブ ⇨ 「アクセス権限」から「ロール名」をクリック
7. 許可ポリシーの「AWSLambdaBasicExecutionRole」をクリック
8. 「編集」で以下の許可ポリシーを追加する
9. 「次へ」 ⇨ 「変更を保存」でポリシーを反映

<details><summary>disconnect関数のコード</summary>

```js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  PutCommand,
  DynamoDBDocumentClient,
  GetCommand,
  DeleteCommand,
  QueryCommand
} from "@aws-sdk/lib-dynamodb";

export const handler = async (event) => {
  const client = new DynamoDBClient({});
  const docClient = DynamoDBDocumentClient.from(client);
  const connectionId = event.requestContext.connectionId;

  const getConnectionIdCommand = new GetCommand({
    TableName: "connections",
    Key: {
      connection_id: connectionId,
    },
  });
  const connection = await docClient.send(getConnectionIdCommand);
  const roomId = connection.Item.joined_room_id;

  const deleteCommand = new DeleteCommand({
    TableName: "connections",
    Key: {
      connection_id: connectionId,
    },
  });
  await docClient.send(deleteCommand);

  const getJoinedRoomConnectionIdsCommand = new QueryCommand({
    TableName: "connections",
    IndexName: "joined_room_id-index",
    KeyConditionExpression: "joined_room_id = :roomId",
    ExpressionAttributeValues: {
      ":roomId": roomId,
    },
  });

  const joinedRoomConnectionIdsResponse = await docClient.send(getJoinedRoomConnectionIdsCommand);
  const joinedRoomConnectionIds = joinedRoomConnectionIdsResponse.Items

  if (joinedRoomConnectionIds.length === 0) {
    const deleteCommand = new DeleteCommand({
      TableName: "rooms",
      Key: {
        room_id: roomId,
      },
    });
    await docClient.send(deleteCommand);
  }

  return {
    statusCode: 200,
  };
};
```

</details>

<details><summary>許可ポリシーの追加</summary>

```diff_json
{
    "Statement": {
        {
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": [
				"dynamodb:*"
			],
			"Resource": "*"
		},
		{
			"Sid": "Statement2",
			"Effect": "Allow",
			"Action": [
				"execute-api:*"
			],
			"Resource": "*"
		}
    }
}
```

</details>

# API Gatewayの作成
## REST API作成
### 手順
1. AWSマネジメントコンソールからAPI Gatewayを検索
2. 右上の「APIを作成」ボタンを押す
3. APIタイプを選択で「Rest API」の「構築」ボタンを押す
4. APIの詳細は以下を設定
    - 新しいAPIを選択
    - API名：`chat-rest-api`
    - エンドポイントタイプ：リージョン

#### Lambda関数の統合（createRoom関数）
1. リソース画面で「メソッドを作成」
2. メソッド詳細は以下を設定
    - メソッドタイプ：POST
    - 統合タイプ：Lambda関数
    - Lambdaプロキシ統合：OFF
    - Lambda関数：`createRoom`
3. 「メソッドを作成」を押す

#### Lambda関数の統合（getAllRooms関数）
1. リソース画面で「メソッドを作成」
2. メソッド詳細は以下を設定
    - メソッドタイプ：GET
    - 統合タイプ：Lambda関数
    - Lambdaプロキシ統合：OFF
    - Lambda関数：`getAllRooms`
3. 「メソッドを作成」を押す

#### その他
1. リソースの詳細画面に戻り、「CORSを無効にする」を押す
2. 「APIをデプロイ」を押す
3. フロントエンドの実装で使用するエンドポイントのURLが発行されます

## Wensocket API作成
### 手順
1. AWSマネジメントコンソールからAPI Gatewayを検索
2. 右上の「APIを作成」ボタンを押す
3. APIタイプを選択で「Websocket API」の「構築」ボタンを押す
4. APIの詳細は以下を設定
    - API名：`chat-websocket-api`
    - ルート選択式：`request.body.action`
5. 「Next」を押す
6. ルートを追加は以下を設定
    - 事前定義されたルートは以下を設定
        - 「$connectルートを追加」を押す
        - 「$disconnectルートを追加」を押す
    - カスタムルートは以下を設定
        - 「カスタムルートを追加」を押す
        - ルートキー：`sendMessage`
7. 「Next」を押す
8. 統合をアタッチは以下を設定
    - $connect
        - 統合タイプ：Lambda
        - Lambda関数：前項で作成したconnect関数
    - $disconnect
        - 統合タイプ：Lambda
        - Lambda関数：前項で作成したdisconnect関数
    - sendMessage
        - 統合タイプ：Lambda
        - Lambda関数：前項で作成したsendMessage関数
9. ステージ名を`production`で「Next」を押す
10. フロントエンドの実装で使用するWebSocket URLが発行されます

# フロントエンドの実装
フロントエンドはNext.JSで作りました。

フォルダ構成
```
── src
   └── app
       ├── _components
       │   └── layouts
       │       └── MainWrpperContent.tsx
       │
       ├── page.tsx
       ├── pages
           ├── create-room
           │   └── page.tsx
           ├── room
           │   └── page.tsx
           └── rooms
               └── page.tsx
```

```ts:src/app/components/layouts/MainWrpperContent.tsx
import { ReactNode } from "react";

type Props = {
  children: ReactNode;
};

export const MainWrapperContent = ({ children }: Props) => {
  return (
    <main className="w-screen h-screen">
      <div className="w-3/4 mx-auto">{children}</div>
    </main>
  );
};
```

```ts:src/app/page.tsx
import Link from "next/link";
import { MainWrapperContent } from "./_components/layouts/MainWrpperContent";

const Home = () => {
  return (
    <MainWrapperContent>
      <div className="py-24 text-center">
        <h1 className="md:text-9xl text-6xl">
          <span className="border-l-[18px] pl-10"></span>Chat
        </h1>
        <ul className="text-center pt-20">
          <li className="py-5 text-xl">
            <Link href="/pages/create-room">Create room</Link>
          </li>
          <li className="py-5 text-xl">
            <Link href="/pages/rooms">Join room</Link>
          </li>
        </ul>
      </div>
    </MainWrapperContent>
  );
};

export default Home;
```

```ts:src/app/pages/create-room/page.tsx
"use client";

import { MainWrapperContent } from "@/app/_components/layouts/MainWrpperContent";
import { useRouter } from "next/navigation";
import { useState } from "react";

const CreateRoom = () => {
  const [roomName, setRoomName] = useState("");
  const router = useRouter();

  const onCreateRoom = async () => {
    if (roomName === "") return;

    const res = await fetch(
      "rest apiのURL",
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ roomName }),
      }
    );
    const data = await res.json();
    const roomId = data.roomId;
    router.push(`/pages/room?roomId=${roomId}&roomName=${roomName}`);
  };

  return (
    <MainWrapperContent>
      <div className="flex flex-wrap items-center justify-center text-center h-screen">
        <div>
          <div className="w-full">
            <input
              type="text"
              placeholder="please room name"
              className="p-2 text-black md:w-96 w-72"
              onChange={(e) => setRoomName(e.target.value)}
            />
          </div>
          <div className="w-full pt-14">
            <button onClick={() => onCreateRoom()}>Create</button>
          </div>
        </div>
      </div>
    </MainWrapperContent>
  );
};

export default CreateRoom;
```

```ts:src/app/pages/room/page.tsx
"use client";

import { MainWrapperContent } from "@/app/_components/layouts/MainWrpperContent";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";

type ReceiveMessage = {
  message: {
    type: "message" | "join";
    message: string;
  };
};

const Room = () => {
  const [socket, setSocket] = useState<WebSocket>();
  const [inputMessage, setInputMessage] = useState("");
  const [receiveMessages, setReceiveMessages] = useState<string[]>([]);
  const searchParams = useSearchParams();
  const roomId = searchParams.get("roomId");
  const roomName = searchParams.get("roomName");

  useEffect(() => {
    if (!roomId) return;

    const socket = new WebSocket(
      `[WebSocket URL]?roomId=${roomId}`
    );
    setSocket(socket);

    socket.onmessage = (event) => {
      const receiveMessage: ReceiveMessage = JSON.parse(event.data);
      if (!receiveMessage?.message?.type) return;
      setReceiveMessages((currentMessages) => {
        return [...currentMessages, receiveMessage.message.message];
      });
    };

    return () => {
      socket.close();
    };
  }, []);

  const sendMessage = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.nativeEvent.isComposing || event.key !== "Enter") return;
    if (inputMessage.trim() === "") return;

    socket?.send(
      JSON.stringify({
        action: "onMessage",
        data: {
          message: inputMessage,
        },
      })
    );
    setInputMessage("");
  };

  return (
    <MainWrapperContent>
      <div className="h-screen">
        <div className="py-5 border-b border-dashed">
          <h1>{roomName}</h1>
        </div>
        <ul className="h-3/4  w-full p-3 overflow-y-auto">
          {receiveMessages.map((message, index) => (
            <li key={index} className="py-2">
              <p>{message}</p>
            </li>
          ))}
        </ul>
        <div className="flex items-center w-full">
          <span>&gt;</span>
          <input
            type="text"
            className="text-black h-9 w-full outline-none p-2 bg-transparent border-b text-white"
            value={inputMessage}
            onChange={(e) => setInputMessage(e.target.value)}
            onKeyDown={sendMessage}
          />
        </div>
      </div>
    </MainWrapperContent>
  );
};

export default Room;

```

```ts:src/app/pages/rooms/page.tsx
"use client";

import { MainWrapperContent } from "@/app/_components/layouts/MainWrpperContent";
import Link from "next/link";
import { useEffect, useState } from "react";

type Room = {
  name: string;
  room_id: string;
};

type Response = {
  statusCode: number;
  body: string;
};

const Rooms = () => {
  const [rooms, setRooms] = useState<Room[]>([]);

  useEffect(() => {
    const getAllRooms = async () => {
      const res = await fetch(
        "https://pdw8ldywkc.execute-api.ap-northeast-1.amazonaws.com/production"
      );

      const resJson: Response = await res.json();

      setRooms(JSON.parse(resJson.body).rooms);
      console.log(JSON.parse(resJson.body).rooms);
    };

    getAllRooms();
  }, []);

  return (
    <MainWrapperContent>
      <div className="py-24">
        <h1 className="text-center text-6xl">Rooms</h1>
      </div>
      <ul className="flex flex-wrap align-center justify-around">
        {rooms.map((room, index) => {
          return (
            <li
              key={index}
              className="w-1/4 my-2 px-2 border-l-4 border-lime-500"
            >
              <Link
                href={`/pages/room?roomId=${room.room_id}&roomName=${room.name}`}
              >
                {room.name}
              </Link>
            </li>
          );
        })}
      </ul>
    </MainWrapperContent>
  );
};

export default Rooms;
```