Next.jsとRails APIモードで最低限のCRUD機能を実装
今度、Next.jsとRailsでWebアプリを作ってみたいと思ったので、
いったん最低限のCRUD機能が実装された、以下のような簡単なタスク管理機能をテンプレート的に用意しようと思います。
大まかな手順
- Rails APIモードのセットアップ
- Next.jsのセットアップ
- RailsでAPIを作成
- Next.jsでAPIを呼び出す関数とUIを作成
Rails APIモードのセットアップ
Dockerを使用してセットアップします。
ステップ1: 必要なファイルの準備
-
プロジェクトディレクトリの作成:
- 任意のフォルダで以下のコマンドを実行。
mkdir myapi cd myapi
-
Gemfileの作成:
-
Gemfile
をプロジェクトのルートに作成します。 -
以下の内容を記述します:
source 'https://rubygems.org' gem "rails", "~> 7.0.7", ">= 7.0.7.2"
-
-
空のGemfile.lockの作成:
-
Gemfile.lock
ファイルを作成しますが、中身は空にします。
-
ステップ2: Dockerfileの作成
-
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の作成
-
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アプリケーションの生成
-
Railsアプリの生成:
docker-compose run web rails new . --force --api --database=postgresql --skip-bundle
ステップ5: データベースの設定
-
データベース設定の変更:
-
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: ビルドとサービスの起動
-
Dockerイメージのビルド:
docker-compose build
-
サービスの起動:
docker-compose up
ステップ7: データベースの作成
-
データベースの作成:
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の設定
-
rack-corsのインストール:
- Gemfileの
gem 'rack-cors'
のコメントを外します
gem 'rack-cors'
- Bundlerを使用してGemをインストールし、再ビルドを行います。
docker-compose run web bundle install docker-compose up --build -d
- Gemfileの
-
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: プロジェクトの作成
-
Railsのディレクトリと共通の親ディレクトリで以下のコマンドを実行。
npx create-next-app@latest
ステップ2: ポート番号を変更
-
ポート番号を 3001 に変更するために package.json を編集。
"scripts": { "dev": "next dev -p 3001", "build": "next build", "start": "next start -p 3001",
RailsでAPIを作成
ここからは、最低限のCRUD操作を実装したタスク管理機能を作成していきます。
ステップ1: Todoモデルの作成
-
Railsアプリケーション内に、Todoモデルを作成します。今回は、title と completed の二つのフィールドを持つようにします。
rails generate model Todo title:string completed:boolean rails db:migrate
ステップ2: Todosコントローラの作成
-
CRUD操作を行うためのTodosコントローラを作成します
rails generate controller Todos
-
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: ルーティングの設定
-
config/routes.rb
にTodoリソースのルートを追加します。resources :todos, only: [:index, :create, :update, :destroy]
Next.jsでAPIを呼び出す関数とUIを作成
基本的に、サーバーコンポーネントでfetch()を使用し、データの取得や送信を行います。
ステップ1: Todoのデータを取得する関数を作成
-
app/components/todo
にTodoGetData.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を送信する関数を作成
-
app/components/todo
にTodoPost.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を更新する関数を作成
-
app/components/todo
にTodoEdit.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を削除する関数を作成
-
app/components/todo
にTodoDelete.ts
を作成し、以下のコードを記述。export default async function TodoDelete(id:string) { await fetch(`http://localhost:3000/todos/${id}`, { method: 'DELETE', }); }
ステップ5: Todoの一覧を表示するページを作成
-
app/todo
にpage.tsx
を作成し、以下のコードを記述。import TodoList from '../components/todo/TodoList'; function Page() { return ( <div> <h1>Todos</h1> <TodoList /> </div> ); } export default Page;
-
app/components/todo
にTodoList.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