👶

Next.jsとRails APIモードで最低限のCRUD機能を実装

2023/12/08に公開

今度、Next.jsとRailsでWebアプリを作ってみたいと思ったので、
いったん最低限のCRUD機能が実装された、以下のような簡単なタスク管理機能をテンプレート的に用意しようと思います。

大まかな手順

  1. Rails APIモードのセットアップ
  2. Next.jsのセットアップ
  3. RailsでAPIを作成
  4. Next.jsでAPIを呼び出す関数とUIを作成

Rails APIモードのセットアップ

Dockerを使用してセットアップします。

ステップ1: 必要なファイルの準備

  1. プロジェクトディレクトリの作成:

    • 任意のフォルダで以下のコマンドを実行。
    mkdir myapi
    cd myapi
    
  2. Gemfileの作成:

    • Gemfile をプロジェクトのルートに作成します。

    • 以下の内容を記述します:

      source 'https://rubygems.org'
      gem "rails", "~> 7.0.7", ">= 7.0.7.2"
      
  3. 空のGemfile.lockの作成:

    • Gemfile.lock ファイルを作成しますが、中身は空にします。

ステップ2: Dockerfileの作成

  1. Dockerfile:
    • Dockerfile を作成し、以下の内容を記述します:

      FROM ruby:3.2.2
      
      RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
      
      WORKDIR /myapi
      
      COPY Gemfile Gemfile.lock /myapi/
      
      RUN bundle install
      
      COPY . /myapi/
      

ステップ3: docker-compose.ymlの作成

  1. docker-compose.yml:
    • docker-compose.yml を作成し、以下の内容を記述します:

      version: '3'
      
      volumes:
        db-data:
      
      services:
        web:
          build: .
          command: bundle exec rails s -p 3000 -b '0.0.0.0'
          volumes:
            - .:/myapi
          environment:
            - POSTGRES_PASSWORD=postgres
            - POSTGRES_USER=user
          ports:
            - "3000:3000"
          depends_on:
            - db
          links:
            - db
      
        db:
          image: postgres:12
          volumes:
            - db-data:/var/lib/postgresql/data
          environment:
            - POSTGRES_PASSWORD=postgres
            - POSTGRES_USER=user
            - POSTGRES_DB=myapi_development
      

ステップ4: Railsアプリケーションの生成

  1. Railsアプリの生成:

    docker-compose run web rails new . --force --api --database=postgresql --skip-bundle
    

ステップ5: データベースの設定

  1. データベース設定の変更:

    • config/database.yml を編集して、データベースの設定をDockerの db サービスに合わせます。
    default: &default
      adapter: postgresql
      encoding: unicode
      host: db
      username: user
      password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
    
    development:
      <<: *default
      database: myapi_development
    

ステップ6: ビルドとサービスの起動

  1. Dockerイメージのビルド:

    docker-compose build
    
  2. サービスの起動:

    docker-compose up
    

ステップ7: データベースの作成

  1. データベースの作成:

    docker-compose run web rails db:create
    docker-compose run web rails db:migrate
    

ステップ8: アプリケーションの確認

  • ブラウザで確認:

    docker compose up --build -d
    
  • http://localhost:3000 にアクセスして、Railsアプリケーションが動作していることを確認します。

ステップ9: CORSの設定

  1. rack-corsのインストール:
    • Gemfileのgem 'rack-cors'のコメントを外します
    gem 'rack-cors'
    
    • Bundlerを使用してGemをインストールし、再ビルドを行います。
    docker-compose run web bundle install
    docker-compose up --build -d
    
  2. rack-corsの設定:
    • ディレクトリ内の config/initializers/cors.rbを開き以下のように修正。
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'http://localhost:3001'  # Next.jsのサーバーのURLを設定
    
        resource "*",
          headers: :any,
          methods: [:get, :post, :put, :patch, :delete, :options, :head]
      end
    end
    

Next.jsのセットアップ

ステップ1: プロジェクトの作成

  1. Railsのディレクトリと共通の親ディレクトリで以下のコマンドを実行。

    npx create-next-app@latest
    

ステップ2: ポート番号を変更

  1. ポート番号を 3001 に変更するために package.json を編集。

    "scripts": {
      "dev": "next dev -p 3001",
      "build": "next build",
      "start": "next start -p 3001",
    

RailsでAPIを作成

ここからは、最低限のCRUD操作を実装したタスク管理機能を作成していきます。

ステップ1: Todoモデルの作成

  1. Railsアプリケーション内に、Todoモデルを作成します。今回は、title と completed の二つのフィールドを持つようにします。

    rails generate model Todo title:string completed:boolean
    rails db:migrate
    

ステップ2: Todosコントローラの作成

  1. CRUD操作を行うためのTodosコントローラを作成します

    rails generate controller Todos
    
  2. Todosコントローラにアクションを実装します。

    class TodosController < ApplicationController
      before_action :set_todo, only: [:update, :destroy]
    
      # GET /todos
      def index
        @todos = Todo.all
        render json: @todos
      end
    
      # POST /todos
      def create
        @todo = Todo.new(todo_params)
        if @todo.save
          render json: @todo, status: :created, location: @todo
        else
          render json: @todo.errors, status: :unprocessable_entity
        end
      end
    
      # PATCH/PUT /todos/:id
      def update
        if @todo.update(todo_params)
          render json: @todo
        else
          render json: @todo.errors, status: :unprocessable_entity
        end
      end
    
      # DELETE /todos/:id
      def destroy
        @todo.destroy
      end
    
      private
        # Use callbacks to share common setup or constraints between actions.
        def set_todo
          @todo = Todo.find(params[:id])
        end
    
        # Only allow a list of trusted parameters through.
        def todo_params
          params.require(:todo).permit(:title, :completed)
        end
    end
    

ステップ3: ルーティングの設定

  1. config/routes.rb にTodoリソースのルートを追加します。

    resources :todos, only: [:index, :create, :update, :destroy]
    

Next.jsでAPIを呼び出す関数とUIを作成

基本的に、サーバーコンポーネントでfetch()を使用し、データの取得や送信を行います。

ステップ1: Todoのデータを取得する関数を作成

  1. app/components/todoTodoGetData.tsを作成し、以下のコードを記述。

    export default async function getData() {
      const response = await fetch('http://localhost:3000/todos');
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      return await response.json();
    }
    

ステップ2: 新規のTodoを送信する関数を作成

  1. app/components/todoTodoPost.tsを作成し、以下のコードを記述。

    export default async function TodoPost(title:string) {
      const response = await fetch('http://localhost:3000/todos', {
    	method: 'POST',
    	headers: {
    	  'Content-Type': 'application/json',
    	},
    	body: JSON.stringify({ todo: { title: title, completed: false } }),
          });
      return response
    }
    

ステップ3: 既存のTodoを更新する関数を作成

  1. app/components/todoTodoEdit.tsを作成し、以下のコードを記述。

    export default async function TodoEdit(id:string, editTitle:string) {
      const response = await fetch(`http://localhost:3000/todos/${id}`, {
        method: 'PATCH', // または 'PUT'
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ todo: { title: editTitle } }),
      });
    }
    

ステップ4: 既存のTodoを削除する関数を作成

  1. app/components/todoTodoDelete.tsを作成し、以下のコードを記述。

    export default async function TodoDelete(id:string) {
      await fetch(`http://localhost:3000/todos/${id}`, {
        method: 'DELETE',
      }); 
    }
    

ステップ5: Todoの一覧を表示するページを作成

  1. app/todopage.tsxを作成し、以下のコードを記述。

    import TodoList from '../components/todo/TodoList';
    
    function Page() {
      return (
        <div>
          <h1>Todos</h1>
          <TodoList />
        </div>
      );
    }
    export default Page;
    
  2. app/components/todoTodoList.tsxを作成し、以下のコードを記述。

    'use client'
    
    import React, { useEffect, useState } from 'react';
    import TodoPost from './TodoPost';
    import TodoDelete from './TodoDelete';
    import getData from './TodoGetData';
    import TodoEdit from './TodoEdit';
    
    // Todoの型定義
    type Todo = {
      id: string;
      title: string;
      completed: boolean;
    };
    
    const TodoComponent = () => {
      const [todos, setTodos] = useState<Todo[]>([]);
      const [title, setTitle] = useState('');
      const [editTitle, setEditTitle] = useState(''); // 編集用のタイトル
      const [editId, setEditId] = useState<string | null>(null);
    
      // Todoリストを取得
      useEffect(() => {
        getData().then(data => {
          setTodos(data);
        });
      }, []);
    
      // Todoを追加する処理
      const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        try {
          const response =await TodoPost(title)
          const newTodo = await response.json();
          setTodos([...todos, newTodo]); // Todoリストを更新
          setTitle('');
        } catch (error) {
          console.error('Failed to create todo:', error);
        }
      };
    
      // Todoを削除する処理
      const deleteTodo = async (id: string) => {
        try {
          TodoDelete(id)
          setTodos(todos.filter(todo => todo.id !== id)); // UIからも削除
        } catch (error) {
          console.error('Failed to delete todo:', error);
        }
      };
    
      // Todo編集処理
      const editTodo = async (id: string) => {
        try {
          await TodoEdit(id, editTitle);
          setTodos(todos.map(todo => todo.id === id ? { ...todo, title: editTitle } : todo));
          setEditId(null);
          setEditTitle(''); // 編集フォームのリセット
        } catch (error) {
          console.error('Failed to edit todo:', error);
        }
      };
    
      return (
        <div className="container mx-auto p-4">
          <form onSubmit={handleSubmit} className="mb-4">
    	<input
    	  type="text"
    	  value={title}
    	  onChange={(e) => setTitle(e.target.value)}
    	  placeholder="Add a new task"
    	  className="shadow appearance-none border rounded py-2 px-3 text-grey-darker mr-2"
    	/>
    	<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    	  Add Task
    	</button>
          </form>
          <ul className="list-disc pl-5">
          {todos.map(todo => (
    	  <li key={todo.id} className="mb-2">
    	    {editId === todo.id ? (
    	      <input
    		type="text"
    		value={editTitle}
    		onChange={(e) => setEditTitle(e.target.value)}
    	      />
    	    ) : (
    	      <span className="text-grey-darker">{todo.title}</span>
    	    )}
    	    <button onClick={() => { setEditId(todo.id); setEditTitle(todo.title); }} className="bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-1 px-2 rounded ml-2">
    	      Edit
    	    </button>
    	    <button onClick={() => deleteTodo(todo.id)} className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded ml-2">
    	      Delete
    	    </button>
    	    {editId === todo.id && (
    	      <button onClick={() => editTodo(todo.id)} className="bg-green-500 hover:bg-green-700 text-white font-bold py-1 px-2 rounded ml-2">
    		Save
    	      </button>
    	    )}
    	  </li>
    	))}
          </ul>
        </div>
      );
    }
    
    export default TodoComponent;
    

これで、RailsとNext.jsのサーバーを立ち上げることで、基本的なタスク管理機能を確認することができると思います。

Discussion