X(旧Twitter)クローン 投稿機能の実装と躓き

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

## 0.前提条件
X(旧Twitter)のクローンサイトの制作過程でつぶやきの投稿機能を実装している際に躓いた内容をご紹介したいと思います。

## 1. 実装内容
以下のコードは実装初期の時点でのコードです。
躓きながらコードを修正していきました。
```Ruby:routes.rb
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'users', controllers: {
        registrations: 'api/v1/registrations'
      }
      resources :tweets, only: %i[create]
    end
  end
```
```Ruby:tweets_controller.rb
class Api::V1::TweetsController < ApplicationController
  before_action :set_user, only: %i[create]
  before_action :authenticate_user!, only: %i[create]

  def create
    @tweet = @user.tweets.new(tweet_params)
    if @tweet.save
      render json: { status: 'SUCCESS', message: 'Saved tweet', data: @tweet }
    else
      render json: { status: 'ERROR', message: 'Tweet not saved', data: @tweet.errors }, status: :unprocessable_entity
    end
  end

  private

  def tweet_params
    params.permit(:user_id, :content, images: [])
  end

  def set_user
    @user = User.find(current_user.id)
  end
end
```
```Ruby:user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
  has_many :tweets, dependent: :destroy
  include DeviseTokenAuth::Concerns::User
end
```
```Ruby:tweet.rb
class Tweet < ApplicationRecord
  belongs_to :user
  has_many_attached :images
end
```
```Ruby:20250603124429_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[7.0]
  def change
    create_table :tweets do |t|
      t.string :content, limit: 140, null: false

      t.timestamps
    end
  end
end
```
## 2. 躓き
今回は上記の通りにつぶやきの投稿機能を実装したので、試しにcurlコマンドを使って投稿できるかどうかを実際にやってみました。
※記憶を遡りながら途中経過を書いたので、途中の内容に若干のズレがあるかもしれまんせ。ご容赦下さい。
- ルートの確認
![tweet create route.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/9e29d914-1518-4b2c-9c3e-93986e95ab21.png)

- curl -X POST -v http\://localhost:3000/api/v1/tweets -d 'content=お試し投稿'
![contentのみのcurlコマンド結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/ae6d156c-9e5a-4aed-a678-0ba546cbb82b.png)
最初のコマンドではTweetテーブルのcontentカラムだけをデータとして付与する送り方でやってみた結果です。
undefiend method method 'id'とあるので、下の部分でidを取得できていないのかな？と推測していました。
```Ruby:tweetes_controll.rb
  def set_user
    @user = User.find(current_user.id)
  end
```

- 色々といじっていると新しいエラーに遭遇しました。修正した内容は不明(申し訳ありません。)
![current_uerが無効な場合の結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/cb70956e-08a7-4bf8-9ce4-bfe5c69f35de.png)
current_userが正しく動作していないことが分かりました。
devise_token_authのメソッドであるcurrent_userはroutesのnamespaceを使った階層に影響を受けることが分かったので下記のように内容を修正しました。
```Ruby:tweets_controller.rb
  def set_user
    @user = User.find(current_api_v1_user.id)
  end
```
また、資料を色々と閲覧している中で認証情報(access-token, uid, client)を一緒に送る必要があることに気付きました。
よって、curlコマンドを下記のように書き換えました。
- curl -X POST -v http\://localhost:3000/api/v1/tweets  -d 'content=お試し投稿' -d 'access-token=xxxxxxx' -d 'client=yyyyy' -d 'uid=zzzzzz'
※認証情報の詳細は割愛しました。

![認証情報を入れた後の最初のcurlコマンド結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/ba18e4c4-2b78-4e56-9d1c-1a38a73410c2.png)
今度はauthenticate_user!が未定義だと指摘されました。
これは先述のcurrent_userのエラーと同じ内容だったので下記のように修正しました。

```Ruby:tweets_controller.rb
before_action :authenticate_api_v1_user!, only: %i[create]
```

authenticate_user!を修正した後に下記のエラーが発生しました。
![authenticate_use!を書き換えた後のエラー文.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/00625ae1-7532-4de9-a4db-2c83a1ac7e1c.png)
400 bad requestなので、こちらから送っているリクエストに問題がありそうです。
これはcontrollerのストロングパラメーターで`require(:tweet)`としているにも関わらず、tweetをキーとして渡していないのが原因でした。
```Ruby:tweets_controller.rb
def tweet_params
    params.require(:tweet).permit(:user_id, :content, images: [])
end
```
最初はtweetキーのことに気付かずに一時的に`require(:tweet)`の記述を外して同じcurlコマンドを実行したところ、エラーの内容が変わりました。
![user_idが見つからないエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/86222e49-5a59-4d4c-85e5-4541ab4768b3.png)
user_idが不明だという指摘に変わっています。

モデル同士のアソシエーションは設定しているので問題無いなどと高を括っていたのですが、その認識が誤っていました。ふと、設定をしていた時のことを思い出していたのですが、マイグレーションのところで外部キーを設定した記憶が無かったのです。。。
```Ruby:20250603124429_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[7.0]
  def change
    create_table :tweets do |t|
      t.string :content, limit: 140, null: false

      t.timestamps
    end
  end
end
```
はい、実際にやっていませんでした。カッコワル...
直ぐにロールバックして、外部キーを設定してマイグレーションをやり直しました。
```Ruby:20250603124429_create_tweets.rb(修正後)
class CreateTweets < ActiveRecord::Migration[7.0]
  def change
    create_table :tweets do |t|
      t.string :content, limit: 140, null: false
      t.references :user, null: false, foreign_key: true #追記

      t.timestamps
    end
  end
end
```
ここで`require(:tweet)`を外した状態でcurlコマンドを実行したところ、コマンドは成功してしまいました。
![ストロングパラメーター修正して仮の成功.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/01f1c05c-d8f3-49ae-b5ba-44f7b57fde7b.png)

`require(:tweet)`の記述を元に戻して、curlコマンドの`content`を渡す部分の記述を書き換えたところ、投稿に成功しました。
curl -X POST -v http\://localhost:3000/api/v1/tweets  -d 'tweet[content]=お試し投稿 2回目' -d 'access-token=xxxxxx' -d 'client=yyyyyy' -d 'uid=`zzzzzzz`'

![curlコマンドを書き換えて最終的に正しく投稿できた.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/4f4a9c68-53bb-4f04-a8a1-f56336263124.png)

これでやっと正しく投稿することに成功しました。

色々な躓きがありましたが、勉強になりました。

ここまで拙い躓きを見て頂きありがとうございました。
読まれた方のエラー解決の一助になれば幸いです。

## 3. 参考にさせて頂いたサイト
[curlコマンド完全ガイド：基礎から現場での実践的な使い方まで](https://qiita.com/kojiro30/items/5ead563c44ad6086ad41)
[[Rails] undefined method authenticate_user!の対処方法](https://qiita.com/kamome_ss/items/38cb953df0d6c2c7167c)
[paramsで値を取得する際にrequireを必ずしも使わなくもいいものなのでしょうか？](https://teratail.com/questions/217838)