React＋Node.js（node-postgres）でRDS上のPostgreSQLと連携する

# 概要
Reactで作ったUIからNode.jsで構えてるAPIを叩き、RDS上のデータを検索して表示するサンプル。
「React RDS 接続」でググったら SpringBoot を使う例しか出てこなくて困ったので、備忘録として記事を作成した。

# 構成
```
Frontend (React) → Backend (Node.js + Express) → PostgreSQL（AWS RDS）
```

# バックエンド

Node.js準備。expressとnode-postgresをインストール。
```bash
mkdir backend && cd backend
npm init -y
npm install express pg cors dotenv
```

バックエンドはindex.jsのみ作成。フロントから受け渡された検索クエリをもとに、RDSを検索、結果を返答する。
ここではRDS上のユーザーをIDで検索する想定。
```javascript:index.jsx
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
require('dotenv').config();

const app = express();
const port = 4000;

const pool = new Pool({
    user: process.env.DB_USER,
    host: process.env.DB_HOST,
    database: process.env.DB_DATABASE,
    password: process.env.DB_PASSWORD,
    port: process.env.DB_PORT,
});

app.use(cors());

app.get('/api/users', async (req, res) => {
  // 検索クエリをセット
  const search = req.query.search;
    
  let query = 'SELECT * FROM '+ process.env.DB_TABLE;
  const params = [];

  if (search) {
    query += ' WHERE id = $1';
    params.push(`%${search}%`);
  }

    try {
        // RDSインスタンスに接続
        const result = await pool.query(query, params);
        res.json(result.rows);
    } catch (error) {
        console.error(error);
        res.status(500).send('Server Error');
    }
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

```

```.env
DB_USER=username
DB_PASSWORD=password
DB_HOST=rds_endpoint
DB_PORT=5432
DB_DATABASE=dbname
DB_TABLE=tablename
```

想定しているRDSの中身
```json:RDSの中身
 [
     {
        "id": 1,
        "name": "田中太郎",
        "email": "tanaka@example.com"
    },
    {
        "id": 2,
        "name": "鈴木花子",
        "email": "suzuki@example.com"
    }
]
```

# フロントエンド
セットアップ。Vite使用。tailwindはv4から色々変わっているので注意。
```bash
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm install axios
npm install tailwindcss @tailwindcss/vite
```

メイン画面作成。検索ボックスと検索結果を表示するだけのシンプル構成。
~~UIセンス皆無なのでcssは生成AIにお任せした。~~ 
```React:src/components/UserList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const UsersList = () => {
  const [users, setUsers] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = (query = '') => {
    axios.get(`http://localhost:4000/api/users?search=${query}`)
      .then(res => setUsers(res.data))
      .catch(err => console.error(err));
  };

  const handleSearch = (e) => {
    e.preventDefault();
    fetchUsers(searchTerm);
  };

  return (
    <div className="min-h-screen bg-gray-100 py-10 px-4 sm:px-6 lg:px-8">
      <div className="max-w-4xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden">
        <div className="px-6 py-5 bg-gradient-to-r from-blue-500 to-indigo-600">
          <h2 className="text-2xl font-bold text-white">ユーザー一覧</h2>
        </div>
        {/* 検索フォーム */}
        <form onSubmit={handleSearch} className="mb-4 flex">
          <input
            type="text"
            placeholder="IDを入力して検索..."
            className="flex-grow px-4 py-2 border border-gray-300 rounded-l-md focus:outline-none"
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
          />
          <button
            type="submit"
            className="px-4 py-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600"
          >
            検索
          </button>
        </form>
        {/* 検索結果表示 */}
        <div className="p-6">
          <table className="min-w-full divide-y divide-gray-200">
            <thead className="bg-gray-50">
              <tr>
                <th className="px-6 py-3 text-left text-sm font-semibold text-gray-600 uppercase tracking-wider">
                    ID
                </th>
                <th className="px-6 py-3 text-left text-sm font-semibold text-gray-600 uppercase tracking-wider">
                    名前
                </th>
                <th className="px-6 py-3 text-left text-sm font-semibold text-gray-600 uppercase tracking-wider">
                    メール
                </th>
              </tr>
            </thead>
            <tbody className="divide-y divide-gray-200">
              {users.map(user => (
                <tr key={user.id} className="hover:bg-gray-50">
                  <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
                      {user.id}
                  </td>
                  <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
                      {user.name}
                  </td>
                  <td className="px-6 py-4 whitespace-nowrap text-sm text-blue-500">
                      {user.email}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

export default UsersList;
```

tailwind使用準備。設定を追加する。
詳細は[公式ドキュメント](https://tailwindcss.com/docs/installation/using-vite)参照。
```diff_css:App.css
+ @import "tailwindcss";

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}
```
```diff_javascript:App.jsx
import React from 'react';
+ import './App.css';
import UsersList from './components/UsersList';

function App() {
  return (
    <UsersList />
  );
}

export default App;
```
```diff_javascript:vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
+    tailwindcss(),
  ],
})

```

# 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4039548/46107dfe-71dd-4c48-94f0-2ee4db448439.png)

# 参考：フォルダ構成
記述に以下を使わせてもらった。便利。

https://tree.nathanfriend.com/
```
.
├── backend/
│   ├── index.js
│   └── .env
└── frontend/
    ├── src/
    │   ├── components/
    │   │   └── UserList.jsx
    │   ├── App.jsx
    │   └── App.css
    └── vite.config.js
```