backendをnest.js
で作成したので、frontendをnext.js
で作成してみる
backendの作成記事はこちら
frontendの作成
プロジェクトの作成
npx create-next-app@latest task-frontend --typescript
今回はプロンプトはすべてEnter
でOK
port設定
今回backendを3000
で起動しているので、frontendは3001
で起動するように編集
package.json
"scripts": {
"dev": "next dev -p 3001",
...
}
この設定によりnpm run dev
で毎回3001
で起動します。
CORS設定(バックエンドで許可必要)
NestJS 側でフロント(localhost:3000)からのアクセスを許可するよう、main.ts
に以下を追加
main.ts
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // ← これが必要!
await app.listen(3000);
}
backendを再起動してください
UI画面の編集
app/page.tsx
'use client';
import { useEffect, useState } from 'react';
type Task = {
id: number;
title: string;
description?: string;
};
export default function Home() {
const [tasks, setTasks] = useState<Task[]>([]);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [editingId, setEditingId] = useState<number | null>(null);
const [editTitle, setEditTitle] = useState('');
const [editDescription, setEditDescription] = useState('');
const fetchTasks = async () => {
const res = await fetch('http://localhost:3000/tasks');
const data = await res.json();
setTasks(data);
};
useEffect(() => {
fetchTasks();
}, []);
const handleCreate = async () => {
await fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
});
setTitle('');
setDescription('');
fetchTasks();
};
const handleDelete = async (id: number) => {
await fetch(`http://localhost:3000/tasks/${id}`, {
method: 'DELETE',
});
fetchTasks();
};
const startEdit = (task: Task) => {
setEditingId(task.id);
setEditTitle(task.title);
setEditDescription(task.description || '');
};
const handleUpdate = async () => {
if (editingId === null) return;
await fetch(`http://localhost:3000/tasks/${editingId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: editTitle, description: editDescription }),
});
setEditingId(null);
setEditTitle('');
setEditDescription('');
fetchTasks();
};
return (
<main className="min-h-screen bg-gray-100 p-8">
<div className="max-w-2xl mx-auto bg-white shadow-lg rounded-lg p-6">
<h1 className="text-2xl font-bold mb-6 text-center text-gray-800">TODOリスト</h1>
<div className="flex flex-col md:flex-row gap-2 mb-6">
<input
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Title"
className="border p-2 flex-1 rounded text-gray-800"
/>
<input
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Description"
className="border p-2 flex-1 rounded text-gray-800"
/>
<button
onClick={handleCreate}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
>
追加
</button>
</div>
<ul className="space-y-4">
{tasks.map(task => (
<li
key={task.id}
className="bg-gray-50 p-4 border rounded flex flex-col md:flex-row md:items-center justify-between"
>
{editingId === task.id ? (
<div className="flex flex-col md:flex-row md:items-center gap-2 w-full">
<input
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className="border p-2 rounded flex-1 text-gray-800"
placeholder="タイトル"
/>
<input
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
className="border p-2 rounded flex-1 text-gray-800"
placeholder="詳細"
/>
<div className="flex gap-2">
<button
onClick={handleUpdate}
className="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded min-w-[64px]"
>
保存
</button>
<button
onClick={() => setEditingId(null)}
className="bg-gray-300 hover:bg-gray-400 px-3 py-2 rounded min-w-[64px]"
>
キャンセル
</button>
</div>
</div>
) : (
<>
<div>
<h3 className="font-semibold text-lg text-gray-800">{task.title}</h3>
<p className="text-gray-600 text-sm">{task.description}</p>
</div>
<div className="flex gap-2 mt-4 md:mt-0">
<button
onClick={() => startEdit(task)}
className="bg-yellow-400 hover:bg-yellow-500 text-white px-3 py-1 rounded"
>
編集
</button>
<button
onClick={() => handleDelete(task.id)}
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
>
削除
</button>
</div>
</>
)}
</li>
))}
</ul>
</div>
</main>
);
}
動作確認
登録、編集、削除問題なく動いていそうですね。
バリデーション
このままでも良いのですが、おまけとして未入力の時のバリデーションを設定しましょう
app/page.tsx
//状態を追加
const [createError, setCreateError] = useState('');
const [editError, setEditError] = useState('');
//登録処理
const handleCreate = async () => {
if (!title.trim()) {
setCreateError('タイトルは必須です');
return;
}
setCreateError('');
await fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
});
setTitle('');
setDescription('');
fetchTasks();
};
//編集処理
const handleUpdate = async () => {
if (!editTitle.trim()) {
setEditError('タイトルは必須です');
return;
}
setEditError('');
await fetch(`http://localhost:3000/tasks/${editingId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: editTitle, description: editDescription }),
});
setEditingId(null);
setEditTitle('');
setEditDescription('');
fetchTasks();
};
登録フォーム下に追加
app/page.tsx
{createError && (
<p className="text-red-500 text-sm mt-1">{createError}</p>
)}
編集フォーム下に追加
app/page.tsx
{editError && (
<p className="text-red-500 text-sm mt-1">{editError}</p>
)}
動作確認
動いていますね
最終UIコード
app/page.tsx
'use client';
import { useEffect, useState } from 'react';
type Task = {
id: number;
title: string;
description?: string;
};
export default function Home() {
const [tasks, setTasks] = useState<Task[]>([]);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [editingId, setEditingId] = useState<number | null>(null);
const [editTitle, setEditTitle] = useState('');
const [editDescription, setEditDescription] = useState('');
const [createError, setCreateError] = useState('');
const [editError, setEditError] = useState('');
const fetchTasks = async () => {
const res = await fetch('http://localhost:3000/tasks');
const data = await res.json();
setTasks(data);
};
useEffect(() => {
fetchTasks();
}, []);
const handleCreate = async () => {
if (!title.trim()) {
setCreateError('タイトルは必須です');
return;
}
setCreateError('');
await fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
});
setTitle('');
setDescription('');
fetchTasks();
};
const handleUpdate = async () => {
if (!editTitle.trim()) {
setEditError('タイトルは必須です');
return;
}
setEditError('');
await fetch(`http://localhost:3000/tasks/${editingId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: editTitle, description: editDescription }),
});
setEditingId(null);
setEditTitle('');
setEditDescription('');
fetchTasks();
};
const handleCancel = () => {
setEditingId(null);
setEditTitle('');
setEditDescription('');
setEditError('');
};
const handleDelete = async (id: number) => {
await fetch(`http://localhost:3000/tasks/${id}`, {
method: 'DELETE',
});
fetchTasks();
};
const startEdit = (task: Task) => {
setEditingId(task.id);
setEditTitle(task.title);
setEditDescription(task.description || '');
};
return (
<main className="min-h-screen bg-gray-100 p-8">
<div className="max-w-2xl mx-auto bg-white shadow-lg rounded-lg p-6">
<h1 className="text-2xl font-bold mb-6 text-center text-gray-800">TODOリスト</h1>
{createError && (
<p className="text-red-500 text-sm mt-1">{createError}</p>
)}
<div className="flex flex-col md:flex-row gap-2 mb-6">
<input
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Title"
className="border p-2 flex-1 rounded text-gray-800"
/>
<input
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Description"
className="border p-2 flex-1 rounded text-gray-800"
/>
<button
onClick={handleCreate}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
>
追加
</button>
</div>
<ul className="space-y-4">
{tasks.map(task => (
<li
key={task.id}
className="bg-gray-50 p-4 border rounded flex flex-col md:flex-row md:items-center justify-between"
>
{editingId === task.id ? (
<div>
{editError && (
<p className="text-red-500 text-sm">{editError}</p>
)}
<div className="flex flex-col md:flex-row md:items-center gap-2 w-full">
<input
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className="border p-2 rounded flex-1 text-gray-800"
placeholder="タイトル"
/>
<input
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
className="border p-2 rounded flex-1 text-gray-800"
placeholder="詳細"
/>
<div className="flex gap-2">
<button
onClick={handleUpdate}
className="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded min-w-[64px]"
>
保存
</button>
<button
onClick={handleCancel}
className="bg-gray-300 hover:bg-gray-400 px-3 py-2 rounded min-w-[64px]"
>
キャンセル
</button>
</div>
</div>
</div>
) : (
<>
<div>
<h3 className="font-semibold text-lg text-gray-800">{task.title}</h3>
<p className="text-gray-600 text-sm">{task.description}</p>
</div>
<div className="flex gap-2 mt-4 md:mt-0">
<button
onClick={() => startEdit(task)}
className="bg-yellow-400 hover:bg-yellow-500 text-white px-3 py-1 rounded"
>
編集
</button>
<button
onClick={() => handleDelete(task.id)}
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded"
>
削除
</button>
</div>
</>
)}
</li>
))}
</ul>
</div>
</main>
);
}
読んでくださりありがとうございました。