コード構築知識ガイド

自分のプロジェクトのコードで学ぶ7テーマ | 英語の意味も全部解説

7テーマ

1Server Actions vs API Routes 2Supabase RLS(Row Level Security) 3Middleware — ページの門番 4Firebase — onAuthStateChanged 5状態管理 — useState / Context / Zustand 6外部API — 署名検証 7テスト — なぜ必要か *まとめ: 7テーマの一言整理
1 / 7

Server Actions vs API Routes

どちらも「サーバー(自分のパソコンじゃなくVercelのコンピュータ)で動くコード」。

Server Actions — 「フォーム送信専用の近道」

endai-system の演題保存コード:

"use server";  // ← この1行で「このファイルはサーバーで動く」と宣言

export async function saveAbstract(data: FormData) {
"use server" = 「サーバーで使う」宣言。書くだけでブラウザから直接呼べるようになる
export = 「外に出す」。他のファイルから使えるようにする
async = 「非同期」。DBの応答を待つ処理に必要(待たないとデータが届く前に次に進む)
FormData = フォーム(入力欄やボタン)から送られてくるデータの入れ物

要するに: ボタンを押す → サーバーで処理 → 結果が画面に反映。これだけ。

API Routes — 「URLで呼べる窓口」

line-crm のWebhook受信コード:

// app/api/webhook/line/route.ts
export async function POST(request: Request) {
POST = HTTPメソッドの1つ。「データを送りつける」という意味。LINEのサーバーが「メッセージ来たよ」とデータを送ってくる
request = 「要求」。送られてきたデータが全部入っている
Request = requestの型(形の定義)。変数の後ろに : 型名 で「この変数はこういう形ですよ」と宣言する

要するに: LINEやStripeなど外部サービスが呼ぶURLを作る仕組み。

使い分けの一言ルール

自分のアプリの画面から呼ぶ → Server Actions
外部サービスから呼ばれる → API Routes

endai-systemが40個もActions持ってるのは全部「画面のフォーム送信」だから。line-crmがAPI Routesだけなのは外部(LINE)からの呼び出しだから。

2 / 7

Supabase RLS(Row Level Security)

RLS = Row Level Security = 「行レベルのセキュリティ」。データベースの行(1件のデータ)ごとに「誰が見ていいか」を制御する仕組み。

Supabase接続コード

endai-system src/lib/supabase/server.ts

import { createServerClient } from "@supabase/ssr";
// ↑「サーバー用の接続係を作る関数」を読み込み

import { cookies } from "next/headers";
// ↑ ブラウザが送ってきたcookie(ログイン情報入りの小さなデータ)を読む関数

export async function createClient() {
  const cookieStore = await cookies();
  // ↑ cookieの保管庫を取得。awaitは「取得完了を待つ」

  return createServerClient(
    SUPABASE_URL,       // データベースの住所
    SUPABASE_ANON_KEY,  // 一般用の鍵(anon = anonymous = 匿名)
    {
      cookies: {
        getAll() { return cookieStore.getAll(); },
        // ↑「今のログイン情報を全部教えて」とSupabaseに渡す
      },
    }
  );
}
この ANON_KEY(匿名の鍵)で接続すると、RLSが効く。つまり「ログインしている人に許可されたデータだけ」が返ってくる。

RLSルールの読み方

CREATE POLICY "著者は自分の演題だけ読める"
  ON abstracts FOR SELECT
  USING (author_id = auth.uid());
CREATE POLICY = 「ルールを作る」
ON abstracts = 「abstractsテーブル(演題の表)に対して」
FOR SELECT = 「読み取り操作について」(SELECT = 選び取る = データを読むこと)
USING (条件) = 「この条件がTRUEのデータだけ」
auth.uid() = 「今ログインしている人のID」(uid = User ID)

つまり: Aさんがログインして全件取得しようとしても、Aさんが著者のデータしか返ってこない。ハッカーが全件取得コードを送りつけても同じ。

Service Role Key(マスターキー)

line-crm にだけある特別な接続方法:

export function createServiceClient() {
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    //          ↑ NEXT_PUBLIC_ がついてない = ブラウザには見えない
    { cookies: { getAll: () => [], setAll: () => {} } }
    //            ↑ cookieは空 = ログインユーザーなしで接続
  );
}
NEXT_PUBLIC_ がつく環境変数 → ブラウザのJavaScriptに含まれる(見える
NEXT_PUBLIC_ がつかない環境変数 → サーバーだけが知っている(見えない
なぜline-crmだけ必要か: LINEから「メッセージ来たよ」と通知が来た時、ログインしているユーザーはいない。RLSは「ログインしている人のデータだけ」返すので、ログインしている人がいないと何もできない。だからRLSを無視できるマスターキーが必要。
3 / 7

Middleware — ページの門番

endai-system src/lib/auth.ts

"use server";

type UserRole = "admin" | "organizer" | "reviewer" | "author";
// ↑ type = 型の定義。UserRole(ユーザーの役割)は4種類のどれか
// | は「または」。決まった文字列しか入らない

const ROLE_HIERARCHY: Record<UserRole, number> = {
  admin: 4, organizer: 3, reviewer: 2, author: 1,
};
// ↑ Record<キーの型, 値の型> = 辞書(キーと値のペア)
// 数字が大きいほど強い権限

export async function requireRole(minimumRole: UserRole): Promise<AuthResult> {
  // ↑ minimumRole = 「最低限必要な役割」
  // Promise<AuthResult> = 「AuthResultを返すことを約束する」(非同期だから)

  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  // ↑ 分割代入(destructuring = 分解して取り出す)
  // supabase.auth.getUser() の結果から data の中の user だけ取り出す
  // 書き換えると:
  //   const result = await supabase.auth.getUser();
  //   const user = result.data.user;  と同じ

  if (!user) {
    throw new Error("認証エラー: ログインが必要です");
    // ↑ throw = 「投げる」。エラーを投げて処理を中断する
  }

  const { data: profile } = await supabase
    .from("profiles")     // profilesテーブルから
    .select("role")       // role列だけ取得
    .eq("id", user.id)    // eq = equals(等しい)。IDが一致する行
    .single();            // 1行だけ(複数あったらエラー)

  const userRole = (profile?.role as UserRole) || "author";
  // ↑ ?. = オプショナルチェイニング。profileがnullでもエラーにならない
  // as UserRole = 型の変換(「これはUserRole型だよ」とTSに教える)
  // || = 「または」。左がnull/undefinedなら右の値を使う

  if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[minimumRole]) {
    throw new Error("権限エラー");
  }

  return { user: { id: user.id, email: user.email }, role: userRole };
}

使い方(Server Actions内で1行呼ぶだけ)

export async function deleteConference(id: string) {
  await requireRole("admin");  // adminじゃないとここでエラー → 下に進めない
  // ... 削除処理
}
4 / 7

Firebase — onAuthStateChanged

bento_order_web の認証コード:

useEffect(() => {
  // ↑ useEffect = 「副作用」
  // コンポーネントが画面に表示された時に1回だけ実行される
  // 「副作用」= 画面の描画以外の処理(データ取得、タイマー、監視など)

  const unsubscribe = onAuthChange(async (fbUser) => {
    // ↑ onAuthChange = 「認証状態が変わったら教えて」とFirebaseに登録
    // fbUser = Firebase User(ログイン中の人。ログアウトするとnull)
    // unsubscribe = 「購読解除」。監視をやめるための関数が返ってくる

    setFirebaseUser(fbUser);
    if (fbUser) {
      const userData = await getUserData(fbUser.uid);
      // ↑ uid = User ID。Firebaseがユーザーごとにつけるユニークなまとめ
      setUser(userData);
    } else {
      setUser(null);  // ログアウト状態
    }
    setLoading(false);
  });

  return () => unsubscribe();
  // ↑ useEffectから関数を返すと「クリーンアップ」になる
  // コンポーネントが画面から消える時に実行される
  // 監視を解除しないとメモリリーク(メモリを食い続ける)になる
}, []);
// ↑ [] = 依存配列が空 = 「最初の1回だけ実行」
// [userId] と書くと「userIdが変わるたびに再実行」

onSnapshot — リアルタイム更新

bento_order_web のメニュー監視:

return onSnapshot(q, (snapshot) => {
  // ↑ onSnapshot = 「データが変わるたびに教えて」
  // snapshot = その瞬間のデータの写真(スナップショット)

  const items = snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
    // ↑ ... = スプレッド演算子。中身を全部展開してコピーする
    // { name: "カレー", price: 500 }
    // → id と合わせて { id: "xxx", name: "カレー", price: 500 }
  }));
  callback(items);
});

共通パターン

onAuthStateChangedonSnapshot は同じ仕組み:
登録 → 変化があるたびにコールバック関数が呼ばれる → 不要になったら unsubscribe で解除

5 / 7

状態管理 — useState / Context / Zustand

useState — 1つの部品の中だけ

const [count, setCount] = useState(0);
// [現在の値, 値を変える関数] = useState(初期値)
// count = 0 から始まる
// setCount(5) で count が 5 になり、画面が自動で再描画される

Context — アプリ全体にデータを共有

bento_order_web の認証Context:

const AuthContext = createContext(null);
// ↑ createContext = 「共有スペース」を作る

export function AuthProvider({ children }) {
  // ↑ children = この中に入る全部の子コンポーネント
  const [user, setUser] = useState(null);

  return (
    <AuthContext.Provider value={{ user }}>
      {children}
    </AuthContext.Provider>
  );
  // ↑ Provider = 「提供者」
  // value に入れたデータが、中の全コンポーネントから使える
}

// 使う側(アプリ内のどこからでも)
const { user } = useContext(AuthContext);
// ↑ useContext = 「共有スペースからデータを取り出す」

Zustand — Contextの進化版

visitcare で使用:

import { create } from "zustand";

const usePatientStore = create((set) => ({
  // ↑ create = ストア(データの倉庫)を作る
  // set = 値を更新する関数
  selectedPatient: null,
  setPatient: (patient) => set({ selectedPatient: patient }),
  // ↑ これを使っているコンポーネント「だけ」が再描画される
  // Contextだと全コンポーネントが再描画されてしまう
}));

// 使う側(Providerで包む必要なし)
const patient = usePatientStore(state => state.selectedPatient);

判断基準

そのコンポーネントだけで使う → useState
ログイン情報のようにめったに変わらない → Context
頻繁に変わる+複数箇所で使う → Zustand
Next.jsでページ表示時にデータ取得 → Server Componentで直接取得(状態管理不要)

6 / 7

外部API — 署名検証

line-crm のWebhook署名検証:

export async function verifySignature(body: string, signature: string) {
  // body = LINEから送られてきたメッセージの中身
  // signature = LINEが「自分が送った証拠」として付けた文字列

  const secret = process.env.LINE_CHANNEL_SECRET!;
  // ↑ ! = 「絶対にnullじゃない」とTSに宣言

  const encoder = new TextEncoder();
  // ↑ 文字列をバイト列(コンピュータが処理できる形)に変換する道具

  const key = await crypto.subtle.importKey(
    'raw',                          // 鍵の形式(生データ)
    encoder.encode(secret),         // secretをバイト列に変換
    { name: 'HMAC', hash: 'SHA-256' },  // 暗号方式
    false,                          // 鍵をエクスポートしない
    ['sign']                        // この鍵は署名に使う
  );
  // ↑ crypto.subtle = 組み込みの暗号ライブラリ
  // importKey = 「鍵を読み込む」

  const signatureBuffer = await crypto.subtle.sign(
    'HMAC', key, encoder.encode(body)
  );
  // ↑ bodyを秘密鍵で暗号化 = 自分で署名を計算する

  const computed = btoa(
    String.fromCharCode(...new Uint8Array(signatureBuffer))
  );
  // ↑ btoa = Binary to ASCII
  // バイナリデータをBase64文字列(安全な文字だけ)に変換

  return computed === signature;
  // 自分で計算した署名 === LINEが送ってきた署名
  // 一致 → 本物のLINEからの通知
  // 不一致 → 偽物(誰かがLINEのふりをしている)
}
原理: LINEと自分だけが知っている SECRET(秘密の文字列)を使って、送られてきたデータを暗号化する。同じSECRETで同じデータを暗号化すれば、必ず同じ結果になる。結果が一致すれば「同じSECRETを知っている = 本物のLINE」と確認できる。
7 / 7

テスト — なぜ必要か

E2Eテスト = ブラウザを自動操作してテスト

endai-system のテスト例:

test("ログインできる", async ({ page }) => {
  // ↑ page = Playwrightが操作する仮想ブラウザ

  await page.goto("/login");
  // ↑ goto = 「行く」。ログインページを開く

  await page.fill('[name="email"]', "test@example.com");
  // ↑ fill = 「埋める」。入力欄にテキストを入力

  await page.click('button[type="submit"]');
  // ↑ click = 送信ボタンをクリック

  await expect(page).toHaveURL("/dashboard");
  // ↑ expect = 「期待する」
  // toHaveURL = 「このURLを持っているはず」
});

一番大事なテスト = CSPチェック

ビルド成功 ≠ 動く。CSP(Content Security Policy = コンテンツ安全方針)の設定ミスはビルドでは見つからない。bento_order_webで実際に事故が起きた。
事故原因ビルドで検出?
fetchが全部失敗CSPのconnect-srcに通信先を書き忘れ
ボタンが全部無反応CSPのscript-srcに'unsafe-inline'がない
ビルド成功なのに本番で白画面CSPがブラウザ実行時にしか効かない

全部Playwrightでブラウザ実行しないと見つからない

プロジェクト別テスト状況

プロジェクトUnitE2ESecurity
endai-system✅ Vitest✅ Playwright (30+)✅ Semgrep
bento_order_web✅ Playwright
line-crm
visitcare
nurse-expense-app
まとめ

7テーマの一言整理

#テーマ一言
1Server Actions vs API Routes画面から→Actions、外部から→Routes
2RLS + Service Role普通の鍵=自分のだけ、マスターキー=全部(Webhook用)
3Middleware認証ページの門番 + データ操作前の二重チェック
4Firebase AuthonAuthStateChanged=ログイン監視、onSnapshot=データ監視
5状態管理小→useState、認証→Context、複雑→Zustand
6署名検証同じ秘密鍵で暗号化→結果一致なら本物
7テストビルド成功≠動く。ブラウザ実行で初めてわかるバグがある

よく出てくる英単語

英語読み方意味
exportエクスポート外に出す(他のファイルから使えるように)
importインポート読み込む(他のファイルから持ってくる)
async / awaitエイシンク / アウェイト非同期 / 待つ(DBやAPIの応答を待つ)
constコンスト定数(一度入れたら変えられない変数)
throwスロー投げる(エラーを投げて処理を中断)
callbackコールバック折り返し電話。後で呼んでもらう関数
subscribe / unsubscribeサブスクライブ購読 / 購読解除(変化の監視を始める/やめる)
destructuringデストラクチャリング分割代入(オブジェクトから必要な部分だけ取り出す)
middlewareミドルウェア中間処理。リクエストがページに届く前に動くコード
policyポリシールール・方針(RLSのアクセスルール)
mutationミューテーション変更。データを書き換えること
queryクエリ問い合わせ。データを取得する命令
schemaスキーマ構造定義。データがどんな形かの設計図
literalリテラル文字通りの値("admin" など固定文字列)
optional chainingオプショナルチェイニング?. nullでもエラーにならない安全なアクセス
spread operatorスプレッド演算子... 中身を全部展開してコピーする