フレームワーク不要!JavaScriptだけで作るCRUDアプリ【MockAPI対応】

JavaScript

前回はmockAPIを使ってREST APIを作成しました。今回はそのAPIを使ってHTML+CSS+JavaScriptで操作するCRUDアプリを作成していきます。
「素の JavaScript で REST API を扱う感覚」がわかる、学習に最適な構成です。

確認

前回作成したAPIは
https://69368643f8dc350aff312a6c.mockapi.io/quotes
にアクセスすると以下のような
– id
– 名言(日本語)
– 名言(英語)
-意味
を返却するAPIです。

今回はこのAPIに接続し、データの作成、一覧、更新、削除ができるいわゆるCRUDアプリを作成していきましょう。

作成

ファイル構成

好きな場所に新規にフォルダを作成し(RomandsDoApp)その中にcssフォルダ、JSフォルダ、index.htmlを作成します。

index.html

まずは以下のようにindex.htmlを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>RomansDo JS Client</title>
  <link rel="stylesheet" href="css/main.css">
  <script type="module" src="js/main.js"></script>
</head>
<body>
<div class="container">
  <h1>RomansDo Quotes Client</h1>
  <p class="status" id="status">読み込み中...</p>

  <!-- 入力フォーム -->
  <form id="quoteForm">
    <input type="hidden" id="quoteId" />

    <div>
      <label for="jp">日本語のことわざ (jp)</label><br />
      <input type="text" id="jp" required />
    </div>
    <div>
      <label for="en">英語 (en)</label><br />
      <input type="text" id="en" required />
    </div>

    <label for="description">説明 (description)</label>
    <textarea id="description"></textarea>

    <div class="buttons">
      <button type="button" class="btn-secondary" id="resetBtn">キャンセル</button>
      <button type="submit" class="btn-primary" id="submitBtn">新規作成</button>
    </div>
  </form>

  <!-- 一覧テーブル -->
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>日本語 (jp)</th>
        <th>英語 (en)</th>
        <th>説明</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody id="quotesBody">
      <!-- JS で埋め込み -->
    </tbody>
  </table>
</div><!--container-->
</body>
</html>  

ポイント解説:type="module" を使った JavaScript 読み込み

<script type="module" src="js/main.js"></script>

この一行が、今回の HTML における 最重要ポイントです。


なぜ type="module" を使っているのか?

type="module" を指定すると、JavaScript ファイルは
ES Modules(モジュール) として読み込まれます。

これにより、従来の <script> にはなかった便利な挙動が
いくつも自動的に有効になります。


① 自動的に strict mode になる

type="module" を指定した JavaScript は、
自動的に strict mode で実行されます。

// 'use strict'; を書かなくてよい
  • 変数の暗黙的なグローバル化を防止
  • 曖昧な挙動や事故りやすい書き方を最初から排除できる

👉 安全な JavaScript がデフォルトになる


② 自動的に defer と同じ挙動になる

モジュールスクリプトは、
HTML の解析が完了してから実行されます。

つまり、次の指定だけで、

<script type="module" src="js/main.js"></script>

以下と同じ効果があります。

<script src="js/main.js" defer></script>
  • DOM 読み込み前に JS が走ってエラーになる心配がない
  • DOMContentLoaded を待つコードが不要になる

👉 DOM 操作をトップレベルに安心して書ける


③ グローバル汚染を防げる

type="module" で読み込まれた JavaScript は、
ファイル単位で独立したスコープを持ちます。

// main.js
const foo = 123;

この foowindow.foo にはなりません。

  • 変数名の衝突を防げる
  • 即時関数で囲む必要がなくなる

👉 昔よく使われていたこの書き方が不要になる

(function () {
  // スコープ確保のための即時関数
})();

まとめ

type="module" を使うことで、

  • strict mode
  • defer 相当の遅延実行
  • グローバル汚染防止

すべてデフォルトで有効になります。

フレームワークを使わなくても、
安全でモダンな JavaScript 開発ができるのが大きなメリットです。

css/main.css

cssフォルダの中にmain.cssファイルを作成し以下のように記述

body {
  margin: 0;
  padding: 24px;
  background: #f5f5f7;
}

h1 {
  margin-top: 0;
}

.container {
  max-width: 960px;
  margin: 0 auto;
  background: #fff;
  padding: 24px;
  border-radius: 12px;
  box-shadow: 0 4px 10px rgba(0,0,0,0.06);
}

form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 16px;
  margin-bottom: 24px;
  align-items: center;
}

form label {
  font-size: 0.9rem;
  color: #555;
}

form input, form textarea {
  width: 100%;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid #ccc;
  font-size: 0.95rem;
  box-sizing: border-box;
}

form textarea {
  grid-column: 1 / 3;
  min-height: 60px;
  /*縦方向のリサイズだけ許可*/
  resize: vertical;
}

.buttons {
  grid-column: 1 / 3;
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

button {
  border: none;
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 0.9rem;
  cursor: pointer;
}

.btn-primary {
  background: #2563eb;
  color: #fff;
}
.btn-secondary {
  background: #e5e7eb;
  color: #111827;
}
.btn-danger {
  background: #ef4444;
  color: #fff;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

th, td {
  border-bottom: 1px solid #e5e7eb;
  padding: 8px 6px;
  vertical-align: top;
}

th {
  text-align: left;
  background: #f9fafb;
}

.actions button {
  margin-right: 4px;
}

.status {
  margin-bottom: 12px;
  font-size: 0.85rem;
  color: #555;
}

display:grid

  • 今回formの要素をgridレイアウトで並べている。gridの知識を深めたい場合は以下のアプリがおすすめ
Grid Garden
A game for learning CSS grid layout

js/main.js

  • jsフォルダの中にmain.jsを作成し以下のように記述
// 👇 自分の MockAPI の URL に書き換える
const BASE_URL = "https://69368643f8dc350aff312a6c.mockapi.io/quotes";

const statusEl = $("#status");
const tbody = $("#quotesBody");

const form = $("#quoteForm");
const quoteIdInput = $("#quoteId");
const jpInput = $("#jp");
const enInput = $("#en");
const descInput = $("#description");
const resetBtn = $("#resetBtn");
const submitBtn = $("#submitBtn");

//DOM要素取得用のショートハンド関数
function $(selector){
  return document.querySelector(selector);
}
// HTML エスケープ
function escapeHtml(str="") {
  return str
    .replace(/&/g, "&")
    .replace(/</g, "<")
    .replace(/>/g, ">")
    .replace(/"/g, """)
    .replace(/'/g, "'");
}

// ステータス表示
function setStatus(message) {
  statusEl.textContent = message;
}

// 一覧取得
async function loadQuotes() {
  setStatus("読み込み中...");
  try {
    const resp = await fetch(BASE_URL);
    if (!resp.ok) throw new Error("HTTP " + resp.status);
    const data = await resp.json();
    renderTable(data);
    setStatus("読み込み完了 (" + data.length + "件)");
  } catch (e) {
    console.error(e);
    setStatus("読み込みに失敗しました: " + e.message);
  }
}

// テーブル描画
function renderTable(quotes) {
  tbody.innerHTML = "";
  quotes.forEach(q => {
    const tr = document.createElement("tr");

    tr.innerHTML = `
      <td>${q.id}</td>
      <td>${escapeHtml(q.jp)}</td>
      <td>${escapeHtml(q.en)}</td>
      <td>${escapeHtml(q.description)}</td>
      <td class="actions"></td>
    `;

    const actionsTd = tr.querySelector(".actions");
    const editBtn = document.createElement("button");
    editBtn.textContent = "編集";
    editBtn.className = "btn-secondary";
    editBtn.onclick = () => startEdit(q);

    const deleteBtn = document.createElement("button");
    deleteBtn.textContent = "削除";
    deleteBtn.className = "btn-danger";
    deleteBtn.onclick = () => deleteQuote(q.id);

    actionsTd.appendChild(editBtn);
    actionsTd.appendChild(deleteBtn);

    tbody.appendChild(tr);
  });
}



// 編集開始
function startEdit(q) {
  quoteIdInput.value = q.id;
  jpInput.value = q.jp || "";
  enInput.value = q.en || "";
  descInput.value = q.description || "";
  submitBtn.textContent = "更新する";
  setStatus("ID " + q.id + " を編集中");
}

// フォームリセット
function resetForm() {
  quoteIdInput.value = "";
  jpInput.value = "";
  enInput.value = "";
  descInput.value = "";
  submitBtn.textContent = "新規作成";
  setStatus("新規作成モード");
}

resetBtn.addEventListener("click", resetForm);

// 作成 or 更新
form.addEventListener("submit", async (e) => {
  // フォームのデフォルト送信(ページ遷移)を無効化
  e.preventDefault();

  const payload = {
    jp: jpInput.value.trim(),
    en: enInput.value.trim(),
    description: descInput.value.trim()
  };

  const id = quoteIdInput.value;
  // 文字列 id を真偽値に変換("" → false, "3" → true)
  const isUpdate = !!id;

  try {
    setStatus(isUpdate ? "更新中..." : "作成中...");
    const resp = await fetch(isUpdate ? `${BASE_URL}/${id}` : BASE_URL, {
      method: isUpdate ? "PUT" : "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload)
    });

    if (!resp.ok) throw new Error("HTTP " + resp.status);

    await loadQuotes();
    resetForm();
    setStatus(isUpdate ? "更新しました" : "作成しました");
  } catch (e2) {
    console.error(e2);
    setStatus("保存に失敗しました: " + e2.message);
  }
});

// 削除
async function deleteQuote(id) {
  if (!confirm(`ID ${id} を削除しますか?`)) return;

  try {
    setStatus("削除中...");
    const resp = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
    if (!resp.ok) throw new Error("HTTP " + resp.status);
    await loadQuotes();
    setStatus("削除しました");
  } catch (e) {
    console.error(e);
    setStatus("削除に失敗しました: " + e.message);
  }
}

// 初期表示
loadQuotes();

ポイント解説①:BASE_URL と API 設定

const BASE_URL = "https://xxxx.mockapi.io/quotes";

この定数には、アクセス先の Web API の URLを定義しています。

  • 一覧取得(GET)
  • 新規作成(POST)
  • 更新(PUT)
  • 削除(DELETE)

すべての通信は、この BASE_URL を基準に行われます。

👉 自分の MockAPI の URL に書き換えるだけで動く設計にしているのがポイントです。


ポイント解説②:DOM 取得用のショートハンド関数 $()

function $(selector){
  return document.querySelector(selector);
}

document.querySelector() を短く書くための
ユーティリティ(ショートハンド)関数です。

const statusEl = $("#status");

のように、jQuery 風の書き方ができるため、

  • コード量が減る
  • DOM 操作が読みやすくなる

というメリットがあります。

👉 機能を増やさず、可読性だけを高める関数です。


ポイント解説③:HTML エスケープ処理(XSS 対策)

function escapeHtml(str="") {
  return str
    .replace(/&/g, "&")
    .replace(/</g, "<")
    .replace(/>/g, ">")
    .replace(/"/g, """)
    .replace(/'/g, "'");
}

API から取得した文字列を
innerHTML で画面に描画する前にエスケープしています。

  • <script> などがそのまま実行されるのを防止
  • ユーザー入力を扱う場合の基本対策

👉 教材レベルでも入れておくと安心な防御コードです。


ポイント解説④:async / await を使った一覧取得

async function loadQuotes() {
  const resp = await fetch(BASE_URL);
  const data = await resp.json();
  renderTable(data);
}
  • fetch() は Promise を返す
  • await によって 通信完了を待ってから次の処理へ進む

その結果、

  • コールバック地獄にならない
  • 同期処理のように直感的に書ける

👉 現代 JavaScript の非同期処理の基本形です。


ポイント解説⑤:DOM を直接生成してテーブルを描画

const tr = document.createElement("tr");
tr.innerHTML = `...`;
tbody.appendChild(tr);

フレームワークを使わず、

  • DOM を生成
  • innerHTML で内容を埋める
  • appendChild で配置

という 素の JavaScript の王道パターンで実装しています。

👉 Vue / React の「再描画」の仕組みの原型とも言える考え方です。


ポイント解説⑥:クロージャを使ったイベント登録

editBtn.onclick = () => startEdit(q);
deleteBtn.onclick = () => deleteQuote(q.id);

forEach 内の q
アロー関数のクロージャとして保持しています。

  • ボタンが押された「未来」でも
  • 対応する q のデータに正しくアクセスできる

👉 イベント駆動 UI の重要な基礎概念です。


ポイント解説⑦:フォーム送信を JavaScript で制御

e.preventDefault();

form 本来の動作である

  • ページ遷移
  • 再読み込み

をキャンセルし、
JavaScript 側で通信・画面更新を制御しています。

👉 ページ遷移しない CRUD アプリの基本形です。


ポイント解説⑧:新規作成か更新かの判定

const isUpdate = !!id;
  • "" → false
  • "3" → true

という JavaScript の真偽値変換を利用しています。

method: isUpdate ? "PUT" : "POST"

👉 1つのフォームで「作成」と「更新」を切り替える定番テクニックです。


ポイント解説⑨:初期表示で一覧を読み込む

loadQuotes();

ページ読み込み時に一度だけ API を呼び、

  • 一覧を取得
  • テーブルを描画

します。

👉 SPA(シングルページアプリ)らしい初期化処理です。


まとめ

この JavaScript は、

  • 非同期通信(fetch / async / await)
  • DOM 操作
  • イベント処理
  • 簡易セキュリティ対策

フレームワークなしで一通り体験できる構成になっています。

「Vue / React を使う前に、
まず素の JavaScript で仕組みを理解する
という目的に非常に向いたサンプルです 👍

実行

  • type=”module”を使っているのでファイルをブラウザにドラッグ&ドロップというわけにはいきません。Webサーバーに配置して実行します。ここではvscodeから簡単に実行できる
    Live Serverを使っていきます。(Live Serverを入れるのは->こちら)
  • vscodeで開き、Open with Live Serverを押す
  • 以下のように表示されれば成功です

最後に

作成、更新、削除などをしてみましょう。非同期通信によるページ遷移のない快適な操作ができるはずです。

今回作成した処理はReactやVueといったJSフレームワークの土台となる考え方です。繰り返し作成し、慣れていくとよいでしょう。

コメント

タイトルとURLをコピーしました