【Next.js×Ruby on Rails連携】管理者画面を作成する方法

# サンプル
管理者画面からPostgreSQLへ登録する方法を備忘録として記録しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/4263dea4-2d02-46ec-a81d-d436cdc76e55.png)


# 画面(Next.js)の実装
登録する際に下記のエラーが出たので覚えとして対象方法を記載します。

```
「ActionController::ParameterMissing (param is missing or the value is empty: adminuser
Did you mean?  adminUserName):

app/controllers/adminusers_controller.rb:49:in adminuser_params'
app/controllers/adminusers_controller.rb:18:in create'
Started POST "/adminusers/create" for ::1 at 2025-03-30 19:55:15 +0900

ActionController::RoutingError (No route matches [POST] "/adminusers/create"):

Started POST "/adminUsers" for ::1 at 2025-03-30 19:58:04 +0900

ActionController::RoutingError (No route matches [POST] "/adminUsers"):

Started POST "/adminusers" for ::1 at 2025-03-30 20:00:39 +0900
Processing by AdminusersController#create as HTML
  Parameters: {"adminUserName"=>"Anderson", "adminUserEmail"=>"anderson@example.com", "adminUserPhone"=>"00111222", "adminUserPassword"=>"[FILTERED]", "adminUserCompany"=>"ABC company", "adminUserDepartment"=>"ABC Depart"}
Completed 400 Bad Request in 1ms (ActiveRecord: 0.0ms | Allocations: 302)



ActionController::ParameterMissing (param is missing or the value is empty: adminuser
Did you mean?  adminUserName):

app/controllers/adminusers_controller.rb:49:in adminuser_params'
app/controllers/adminusers_controller.rb:18:in create'」
```

具体的には、エラーメッセージに「param is missing or the value is empty: adminuser」と書かれており、params.require(:adminuser) が必要なパラメータを見つけられないという状況です。

Railsでは、params に渡すデータの名前が一致している必要がありますが、Next.js側の FormData では、adminUserName などのキーを渡しているため、Rails側での adminuser_params が適切に動作しません。Rails側では、params.require(:adminuser) を使って adminuser というキーを期待しているため、リクエストが一致しません。

解決策
Next.jsから送るパラメータ名を修正する: Rails側で adminuser_params が :adminuser を期待しているので、Next.js側でもその形式に合わせる必要があります。具体的には、FormData ではなく、JSON形式でデータを送信し、キー名を adminuser にラップする方法です。

Next.js側の変更: 現在、Next.jsで送信しているデータは、FormData として送信していますが、RailsはJSONを期待しているので、JSON形式でデータを送るように変更します。

:::note info
#### FormをPost送信する際の注意点
今回のコードの場合、onSubmitの引数は、handleSubmit関数の中に引数を入れましょう。
そうでないと、サーバ側にデータがPOST送信されません。
```page.tsx
 <form onSubmit={handleSubmit(onSubmit)}>
 </form>
```
:::


```frontend/app/adminUsers/registration/page.tsx
'use client'
import { useState,useEffect } from "react"
import { useRouter } from "next/navigation"
import axios from "axios"
import { useForm } from "react-hook-form"


export default function AdminUserRegistration(){
    const router = useRouter();
    const defaultValues = {
        adminUserName:'',
        adminUserEmail:'',
        adminUserPhone:'',
        adminUserPassword:'',
        adminUserCompany:'',
        adminUserDepartment:''
    };

    const {register,handleSubmit,formState:{errors}} = useForm({
        defaultValues
    });
    const [adminUserName,setAdminUserName] = useState('');
    const [adminUserEmail,setAdminUserEmail] = useState('');
    const [adminUserPhone,setAdminUserPhone] = useState('');
    const [adminUserPassword,setAdminUserPassword] = useState('');
    const [adminUserCompany,setAdminUserCompany] = useState('');
    const [adminUserDepartment,setAdminUserDepartment] = useState('');

    const onSubmit = async (data: any) => {
        const formData = new FormData();
        formData.append('adminUserName',adminUserName);
        formData.append('adminUserEmail',adminUserEmail);
        formData.append('adminUserPhone',adminUserPhone);
        formData.append('adminUserPassword',adminUserPassword);
        formData.append('adminUserCompany',adminUserCompany);
        formData.append('adminUserDepartment',adminUserDepartment);
        try {
            const response = await axios.post('http://localhost:3000/adminusers', {
                adminuser: {  // adminuserというラッパーを使う
                    adminUserName: data.adminUserName,
                    adminUserEmail: data.adminUserEmail,
                    adminUserPhone: data.adminUserPhone,
                    adminUserPassword: data.adminUserPassword,
                    adminUserCompany: data.adminUserCompany,
                    adminUserDepartment: data.adminUserDepartment
                }
            }, {
                headers: {
                    'Content-Type': 'application/json'  // 明示的にJSON形式で送信
                }
            });

            if (response.status === 200) {
                alert('Admin User Registered successfully');
                console.log(response.data);
            } else {
                console.log(response.data);
            }
        } catch (error) {
            console.error(error);
            alert('Error occurred while registering the admin user');
        }
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label>Name</label>
                <input
                    type="text"
                    className="w-full border border-gray-500 rounded-md"
                    defaultValue={defaultValues.adminUserName}
                    {...register('adminUserName',{
                        required:'AdminUserNamwe must be required.'
                    })}
                    onChange={(e)=>setAdminUserName(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserName?.message}</div>
            </div>
            <div>
                <label>Email</label>
                <input
                    type="email"
                    className="w-full border border-gray-500 rounded-md"
                    defaultValue={defaultValues.adminUserEmail}
                    {...register('adminUserEmail',{
                        required:'AdminuserEmail must be required.'
                    })}
                    onChange={(e)=>setAdminUserEmail(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserEmail?.message}</div>
            </div>
            <div>
                <label>Phone</label>
                <input
                    type="text"
                    className="w-full border border-gray-500 rounded-md"
                    defaultValue={defaultValues.adminUserPhone}
                    {...register('adminUserPhone',{
                        required:'AdminUserPhone must be required.'
                    })}
                    onChange={(e)=>{setAdminUserPhone(e.target.value)}}
                />
                <div className="text-rose-500">{errors.adminUserPhone?.message}</div>
            </div>
            <div>
                <label>Password</label>
                <input
                    type="password"
                    className="w-full border border-gray-500 rounded-md"
                    defaultValue={defaultValues.adminUserPassword}
                    {...register('adminUserPassword',{
                        required:'AdminUserPassword must be required.'
                    })}
                    onChange={(e)=>setAdminUserPassword(e.target.value)}
                />
                <div className="text-rose-500">{errors.adminUserPassword ?.message}</div>
            </div>
            <label>Company</label>
            <input
                type="text"
                className="w-full border border-gray-500 rounded-md"
                defaultValue={defaultValues.adminUserCompany}
                {...register('adminUserCompany',{
                    required:'Company must be required.'
                })}
                onChange={(e)=>setAdminUserCompany(e.target.value)}
            />
            <div className="text-rose-500">{errors.adminUserCompany?.message}</div>
            <div>
                <label>Department</label>
                <input
                    type="text"
                    className="w-full border border-gray-500 rounded-md"
                    defaultValue={defaultValues.adminUserDepartment}
                    {...register('adminUserDepartment',{
                        required:'Departmanet muset be required.'
                    })}
                    onChange={(e)=>{setAdminUserDepartment(e.target.value)}}
                />
                <div className="text-rose-500">{errors.adminUserDepartment?.message}</div>
            </div>
            <div>
                <button
                    type="submit"
                    className="bg bg-blue-500 text-white rounded-md px-4 py-4"
                >
                    Register
                </button>
                <button
                    type="button"
                    className="bg bg-green-500 text-white rounded-md px-4 py-4"
                >
                    Back to Top
                </button>
            </div>
        </form>
    )
}
```

# サーバ側(Ruby on Rails)の処理


#### bcrypt Gemのインストール 
まず、bcrypt ジェムをインストールします。これを使用すると、パスワードを安全にハッシュ化できます。

Gemfileに以下を追加します。
```
gem 'bcrypt', '~> 3.1.7'
```

そして、次のコマンドでインストールします。
```
bundle install
```



```backend/controllers/adminusers_controller.rb
class AdminusersController < ApplicationController
  before_action :set_adminuser, only: %i[ show update destroy ]

  # GET /adminusers
  def index
    @adminusers = Adminuser.all

    render json: @adminusers
  end

  # GET /adminusers/1
  def show
    render json: @adminuser
  end

  # POST /adminusers
  def create
    @adminuser = Adminuser.new(adminuser_params)

    #Generate hash password
    @adminuser.adminUserPassword = BCrypt::Password.create(@adminuser.adminUserPassword)

    if @adminuser.save
      render json: @adminuser, status: :created, location: @adminuser
    else
      render json: @adminuser.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /adminusers/1
  def update
    if @adminuser.update(adminuser_params)
      render json: @adminuser
    else
      render json: @adminuser.errors, status: :unprocessable_entity
    end
  end

  # DELETE /adminusers/1
  def destroy
    @adminuser.destroy!
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_adminuser
      @adminuser = Adminuser.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def adminuser_params
      params.require(:adminuser).permit(:adminUserName, :adminUserEmail, :adminUserPhone, :adminUserPassword, :adminUserCompany, :adminUserDepartment)
    end
end

```