CRUD操作によるデータ管理 (Svelte & MongoDB)
1. +page.server.ts (サーバーサイド)
この部分はサーバーサイドのロジックを担当しています。主に認証とデータベース操作を行います。
ロード関数
export const load: PageServerLoad = async ({ locals }) => {
await connect_to_db(); // データベースに接続
const session = await locals.auth.validate();
if (!session) {
throw redirect(303, "/login"); // ログインしていない場合はログインページにリダイレクト
}
// 記事を取得
const articles = await Article.find({ user_id: session.user.userId })
.sort({ createdAt: -1 }) // 作成日時で降順にソート
.select("_id title content createdAt updatedAt"); // 必要なフィールドのみ選択
return {
articles: JSON.parse(JSON.stringify(articles)), // Mongoose ドキュメントをプレーンなオブジェクトに変換
user: session.user, // ログインユーザー情報
};
};
このロード関数は以下の処理を行います:
- データベースに接続
- ユーザーのセッションを検証し、ログインしていなければログインページにリダイレクト
- ログインユーザーの記事を作成日時の降順で取得
- 記事データとユーザー情報をページコンポーネントに渡す
アクション関数
export const actions = {
create: async ({ request, locals }) => {
await connect_to_db(); // データベースに接続
const session = await locals.auth.validate();
if (!session) return fail(401, { error: "Unauthorized" }); // ログインしていない場合はエラー
const formData = await request.formData();
const title = formData.get("title")?.toString();
const content = formData.get("content")?.toString();
if (!title || !content) {
return fail(400, { error: "Title and content are required" }); // タイトルと内容が必須
}
try {
// 記事を作成
await Article.create({
title,
content,
user_id: session.user.userId, // ユーザーIDを設定
});
} catch (error) {
console.error(error);
return fail(500, { error: "Failed to create article" }); // エラー処理
}
return { success: true };
},
update: async ({ request, locals }) => {
await connect_to_db(); // データベースに接続
const session = await locals.auth.validate();
if (!session) return fail(401, { error: "Unauthorized" }); // ログインしていない場合はエラー
const formData = await request.formData();
const id = formData.get("id")?.toString();
const title = formData.get("title")?.toString();
const content = formData.get("content")?.toString();
if (!id || !title || !content) {
return fail(400, { error: "All fields are required" }); // すべてのフィールドが必須
}
try {
// 記事を更新
await Article.findByIdAndUpdate(id, {
title,
content,
updatedAt: new Date(), // 更新日時を設定
});
} catch (error) {
console.error(error);
return fail(500, { error: "Failed to update article" }); // エラー処理
}
return { success: true };
},
delete: async ({ request, locals }) => {
await connect_to_db(); // データベースに接続
const session = await locals.auth.validate();
if (!session) return fail(401, { error: "Unauthorized" }); // ログインしていない場合はエラー
const formData = await request.formData();
const id = formData.get("id")?.toString();
if (!id) {
return fail(400, { error: "Article ID is required" }); // 記事IDが必須
}
try {
// 記事を削除
await Article.findByIdAndDelete(id);
} catch (error) {
console.error(error);
return fail(500, { error: "Failed to delete article" }); // エラー処理
}
return { success: true };
},
logout: async ({ locals }) => {
const session = await locals.auth.validate();
if (!session) return fail(401); // ログインしていない場合はエラー
// セッションを無効化
await auth.invalidateSession(session.sessionId);
// ローカルのセッションをクリア
locals.auth.setSession(null);
// ログインページにリダイレクト
throw redirect(303, "/login");
},
} satisfies Actions;
アクション関数では4つの処理を実装しています:
create
: 新しい記事の作成update
: 既存記事の更新delete
: 記事の削除logout
: ユーザーのログアウト処理
各アクションは認証チェックを行い、フォームデータを検証した上でデータベース操作を実行します。
2. +page.svelte (クライアントサイド)
スクリプト部分
<script lang="ts">
import { enhance } from "$app/forms";
import { invalidateAll } from "$app/navigation";
import type { PageData } from "./$types";
import type { SubmitFunction } from "@sveltejs/kit";
interface Article {
_id: string;
title: string;
content: string;
createdAt: Date;
updatedAt: Date;
}
export let data: PageData;
let editingArticle: Article | null = null;
let showCreateForm = false;
let updating = false;
const handleSubmit: SubmitFunction = () => {
return async ({ update }) => {
updating = true;
try {
await update(); // フォーム送信を処理
await invalidateAll(); // ページをリフレッシュ
editingArticle = null; // 編集モードを終了
showCreateForm = false; // 作成フォームを非表示
} catch (error) {
console.error("Operation failed", error); // エラーハンドリング
} finally {
updating = false;
}
};
};
function startEdit(article: Article) {
editingArticle = { ...article }; // 編集モードを開始
}
function cancelEdit() {
editingArticle = null; // 編集モードをキャンセル
}
function confirmDelete(event: Event) {
if (!confirm("Are you sure you want to delete this article?")) {
event.preventDefault(); // 削除をキャンセル
}
}
</script>
スクリプト部分では:
- 必要なインポートと型定義
- 状態変数の定義(編集中の記事、フォーム表示状態、更新中フラグ)
- フォーム送信ハンドラー関数(
handleSubmit
) - 編集開始/キャンセル関数
- 削除確認関数
enhance
ディレクティブを使用して、ページ全体をリロードせずにフォーム送信をハンドリングしています。
ヘッダー部分のHTML
<div class="min-h-screen bg-gray-50 max-w-5xl mx-auto py-4">
<div class="max-w-4xl mx-auto px-4">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">My Articles</h1>
<div class="flex gap-4">
<form action="?/logout" method="POST" use:enhance>
<button
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition-colors"
type="submit"
>
Logout
</button>
</form>
{#if !showCreateForm}
<button
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors"
on:click={() => (showCreateForm = true)}
>
New Article
</button>
{/if}
</div>
</div>
ヘッダー部分には:
- ページタイトル(「My Articles」)
- ログアウトボタン
- 新規記事作成ボタン(フォームが表示されていない場合のみ)
新規記事作成フォーム
{#if showCreateForm}
<div class="mb-8 bg-white rounded-lg shadow p-6">
<form
method="POST"
action="?/create"
use:enhance={handleSubmit}
class="space-y-4"
>
<div>
<label for="title" class="block text-sm font-medium text-gray-700"
>Title</label
>
<input
type="text"
id="title"
name="title"
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700"
>Content</label
>
<textarea
id="content"
name="content"
required
rows="4"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
></textarea>
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="px-4 py-2 text-gray-700 hover:text-gray-900"
on:click={() => (showCreateForm = false)}
disabled={updating}
>
Cancel
</button>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50"
disabled={updating}
>
Create
</button>
</div>
</form>
</div>
{/if}
新規記事作成フォームは条件付きでレンダリングされ:
- タイトル入力フィールド
- 内容入力用テキストエリア
- キャンセルボタンと作成ボタン
?/create
アクションを呼び出すフォーム設定
記事一覧表示部分
<div class="space-y-6">
{#each data.articles as article (article._id)}
<div class="bg-white rounded-lg shadow p-6">
{#if editingArticle !== null && editingArticle._id === article._id}
<form
method="POST"
action="?/update"
use:enhance={handleSubmit}
class="space-y-4"
>
<input type="hidden" name="id" value={article._id} />
<div>
<label
for="edit-title"
class="block text-sm font-medium text-gray-700">Title</label
>
<input
type="text"
id="edit-title"
name="title"
bind:value={editingArticle.title}
required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label
for="edit-content"
class="block text-sm font-medium text-gray-700">Content</label
>
<textarea
id="edit-content"
name="content"
bind:value={editingArticle.content}
required
rows="4"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
></textarea>
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="px-4 py-2 text-gray-700 hover:text-gray-900"
on:click={cancelEdit}
disabled={updating}
>
Cancel
</button>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50"
disabled={updating}
>
Save
</button>
</div>
</form>
{:else}
<div>
<div class="flex justify-between items-start mb-4">
<h2 class="text-xl font-semibold text-gray-900">
{article.title}
</h2>
<div class="flex gap-2">
<button
class="text-blue-500 hover:text-blue-700 transition-colors"
on:click={() => startEdit(article)}
>
Edit
</button>
<form
method="POST"
action="?/delete"
use:enhance={handleSubmit}
class="inline"
>
<input type="hidden" name="id" value={article._id} />
<button
type="submit"
class="text-red-500 hover:text-red-700 transition-colors"
on:click={confirmDelete}
>
Delete
</button>
</form>
</div>
</div>
<p class="text-gray-600 mb-4 whitespace-pre-wrap">
{article.content}
</p>
<div class="text-sm text-gray-500">
Created: {new Date(article.createdAt).toLocaleDateString()}
{#if article.updatedAt !== article.createdAt}
(Updated: {new Date(article.updatedAt).toLocaleDateString()})
{/if}
</div>
</div>
{/if}
</div>
{:else}
<p class="text-center text-gray-500 py-8">
No articles yet. Create your first one!
</p>
{/each}
</div>
記事一覧部分では:
{#each}
ブロックで記事のリストをループ- 編集中の記事の場合は編集フォームを表示
- 記事IDを隠しフィールドで送信
- タイトルと内容を
bind:value
で双方向バインディング - キャンセルと保存のボタン
- 通常表示モードでは:
- タイトル、編集・削除ボタン
- 記事内容
- 作成日時と更新日時の表示
- 記事がない場合は代替メッセージを表示
まとめ
このSvelteKitアプリケーションは、基本的なCRUD操作(作成・読み取り・更新・削除)を実装した記事管理システムです。
+page.server.ts の役割:
- 認証チェック
- サーバーサイドでのデータ取得(記事一覧)
- フォームアクションの処理(作成、更新、削除、ログアウト)
- データベース操作
+page.svelte の役割:
- ユーザーインターフェースの提供
- 状態管理(編集モード、フォーム表示など)
- フォーム送信のエンハンス(
enhance
ディレクティブ使用) - 条件付きレンダリング(編集モード/表示モードの切り替え)
この実装はSvelteKitのフォーム処理機能を活用し、ページリロードなしで操作を完結させる優れたUXを提供しています。