2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js × Supabase × Vercel】匿名お悩み共感ボード「Moyan」をClaude Codeとゼロから1時間で作って公開した

2
Posted at

はじめに

「もやもやしてるけど、誰にも言えない…」

そんな気持ちを匿名で吐き出して、共感ボタンで救われる場所を作りました。

Moyan(モヤン) — 匿名お悩み共感ボード
🔗 https://moyan-ten.vercel.app/

今回は Claude Code(AI)と一緒に設計書から作り始めて、公開まで一気に完走しました。
「コードを全部自分で書く」のではなく「何を作るか・どう設計するか」に集中する開発スタイルの記録でもあります。


作ったもの

項目 内容
サービス名 Moyan(モヤン)
コンセプト 匿名でもやもやを吐き出し、共感で救われる
ターゲット 誰でも(認証不要・完全匿名)
URL https://moyan-ten.vercel.app/

機能

  • 匿名投稿(テキスト300文字・カテゴリ選択)
  • 新着順 / 共感順の切り替え
  • カテゴリフィルター(仕事・人間関係・家族・恋愛・お金・その他)
  • 共感ボタン(同じブラウザからは1回だけ押せる)
  • 報告ボタン

開発の進め方:設計書から始める

今回こだわったのが「仕様が曖昧なまま作り始めない」こと。

いきなりコードを書き始める前に、Claude Codeと一緒に設計書(DESIGN.md)を作りました。

1. 設計書作成(機能・画面・API・DB設計を先に決める)
2. package.json 作成・パッケージ導入
3. Expressサーバー(server.js)作成
4. Supabase 設定・テーブル作成
5. API実装(投稿・共感・報告)
6. フロントエンド実装(HTML/CSS/JS)
7. ローカル動作確認
8. GitHub → Vercel でデプロイ

設計書を先に作ることで「何をどこまで作るか」がブレなくなり、実装がスムーズになりました。
特にMVPとPhase 2を分ける判断は早めにしておくと、作りすぎを防げます。

Claude Codeとの協働スタイル

  • 自分がやること: 何を作るか決める・選択肢を選ぶ・Supabaseなどのブラウザ操作
  • Claude Codeがやること: コードを書く・エラーを調べる・設計書を作る

「AIに全部任せる」ではなく「意思決定は自分・実装はAI」という分担です。
コードをあまり読まなくても動くものが作れてしまうのがAI開発の面白さで、怖さでもあります。


技術スタック

レイヤー 技術 選んだ理由
Runtime Node.js v24 環境構築済みで即スタートできる
Framework Express v4 定番・情報が豊富
DB Supabase(PostgreSQL) 無料・アカウント既存・トレンド
DB Client @supabase/supabase-js Supabase公式クライアント
Frontend Vanilla HTML / CSS / JS フレームワーク不要でシンプルに
Deploy Vercel(無料) GitHubと連携・Node.js対応

プロジェクト構成

moyan/
├── server.js          # Expressサーバーのエントリーポイント
├── lib/
│   └── supabase.js    # Supabaseクライアントの初期化
├── routes/
│   └── posts.js       # 投稿・共感・報告のAPI
├── public/
│   ├── index.html     # 画面のHTML
│   ├── style.css      # スタイル
│   └── app.js         # フロントエンドのロジック
├── vercel.json        # Vercelデプロイ設定
└── .env               # 環境変数(Gitには含めない)

実装のポイント

1. Supabaseのテーブル設計

CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  text TEXT NOT NULL CHECK (char_length(text) <= 300),
  category TEXT NOT NULL CHECK (
    category IN ('仕事', '人間関係', '家族', '恋愛', 'お金', 'その他')
  ),
  sympathy_count INTEGER DEFAULT 0,
  report_count INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

カテゴリはCHECK制約で値を限定しています。API側だけでなくDB側でも制約をかけることで二重のバリデーションになります。


2. 共感カウントの競合問題をRPCで解決

共感ボタンのカウントアップを単純に「現在値 + 1」で実装すると、2人が同時に押したとき片方の更新が消えてしまいます(競合問題)。

これを防ぐため、PostgreSQL関数(RPC)でアトミックに処理します。

CREATE OR REPLACE FUNCTION increment_sympathy(post_id UUID)
RETURNS void AS $$
BEGIN
  UPDATE posts SET sympathy_count = sympathy_count + 1 WHERE id = post_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

SECURITY DEFINERをつけることで、DBオーナー権限で実行されRLS(後述)を回避できます。

Node.js側はシンプルに呼ぶだけ:

const { error } = await supabase.rpc('increment_sympathy', { post_id: id });

3. SupabaseのRLS(Row Level Security)設定

SupabaseはデフォルトでRLSが有効で、すべてのアクセスをブロックします。
匿名サービスなので「誰でも読める・誰でも投稿できる」ポリシーを追加します。

-- 誰でも閲覧できる
CREATE POLICY "誰でも閲覧可能" ON posts
  FOR SELECT USING (true);

-- 誰でも投稿できる
CREATE POLICY "誰でも投稿可能" ON posts
  FOR INSERT WITH CHECK (true);

ポイントは、increment_sympathy関数にSECURITY DEFINERをつけたのでUPDATEポリシーは不要になる点です。


4. XSS対策

ユーザーが投稿したテキストをそのままHTMLに埋め込むとXSS攻撃の危険があります。
innerHTMLを使わず、特殊文字をエスケープしてから表示します。

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

5. ローカル ↔ 本番でAPIのURLを自動切り替え

const API_BASE = window.location.hostname === 'localhost'
  ? 'http://localhost:3001/api'
  : '/api';

ローカル開発時は localhost:3001、Vercel上では /api を自動で使い分けます。


6. vercel.json でExpressをサーバーレスとして動かす

VercelはデフォルトではNext.js向けのため、Expressを動かすには設定が必要です。

{
  "version": 2,
  "builds": [
    { "src": "server.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/(.*)", "dest": "server.js" }
  ]
}

7. 共感済み管理はlocalStorageで

「同じ人が何度も共感できないように」する方法として、今回はlocalStorageを採用しました。

  • メリット: サーバー負荷ゼロ・認証不要で実現できる
  • デメリット: ブラウザをまたいだり、ストレージを消せば再度押せる

匿名サービスのMVPとしては十分なアプローチです。

function getSympathized(postId) {
  const list = JSON.parse(localStorage.getItem('sympathized') || '[]');
  return list.includes(postId);
}

API設計

メソッド パス 説明
GET /api/posts 投稿一覧(sort・category・page対応)
POST /api/posts 新規投稿
POST /api/posts/:id/sympathy 共感
POST /api/posts/:id/report 報告

デプロイまでの流れ

ローカルで開発
    ↓ git push
GitHub リポジトリ(https://github.com/mstng/moyan)
    ↓ 自動デプロイ(Vercel連携)
Vercel(環境変数にSUPABASE_URL・SUPABASE_ANON_KEYを設定)
    ↓
本番公開 🎉

VercelとGitHubを連携させておくと、git pushするたびに自動でデプロイされます。


つまずいたポイント

ポート競合

ローカルで起動しようとしたらポート3000が別サービスに使われていた。.envでPORTを3001に変更して解決。

RLSでINSERTが弾かれた

デプロイ後に投稿が row-level security policy エラーで失敗。SupabaseのRLSポリシーを追加して解決。

認証情報の誤ったファイル管理

パスワードをメモしたファイルをうっかりGitにステージングしてしまった。.gitignoreに追加して除外。認証情報は.envだけに書くが鉄則。


まとめ

Node.js + Express + Supabase + Vercel の組み合わせで、設計から公開まで一気に体験できました。

SupabaseはRLSやRPC関数など最初につまずきやすいポイントがありますが、一度理解すると応用が効きます。

Claude Codeと一緒に作って気づいたこと

コードをあまり意識しなくても、サービスが公開できてしまいました。
ただ、今日触れた概念——RLS・RPC・XSSエスケープ・ミドルウェア——はエンジニアと技術議論するための共通言語として確実に身につきます。

「自分で書かない」=「理解しなくていい」ではなく、なぜそう動くのかを理解しながらAIに実装させるスタイルが、マネージャーエンジニアとして一番効率的だと感じました。

実際に動くURLがあるのはやっぱり嬉しいです 😶‍🌫️

🔗 Moyan https://moyan-ten.vercel.app/

もやもやがあったらぜひ投稿してみてください。


参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?