Express + Mongoose + EJSで簡易アプリを作る

# 概要
ExpressとMongooseで簡単な商品管理アプリを作成しました。
テンプレートエンジンはEJSを使用しています。

# インストール

Express、EJS、Mongooseをインストールして`index.js`と`views`ディレクトリを作成します。
```zsh
npm i express ejs mongoose
touch index.js
mkdir views
```

作成時のパッケージのバージョンは以下です。
ejs: `"^3.1.10"`
express: `"^4.19.2"`
mongoose: `"^8.3.3"`

次にExpress、EJS、Mongooseを設定します。
```javascript:index.js
const express = require("express");
const app = express();
const path = require("path");

// Mongooseの設定
const mongoose = require("mongoose");
main().catch((err) => console.log(err));

async function main() {
  await mongoose.connect("mongodb://127.0.0.1:27017/shopApp");
  console.log("db connected");
}

app.set("views", path.join(__dirname), "views");
app.set("view engine", "ejs");

app.listen(3000, () => {
  console.log("watching...");
});
```

nodemonを使ってサーバーを起動します。
```zsh
nodemon index.js
```
https://www.npmjs.com/package/nodemon

# modelの作成
`models`ディレクトリに`project.js`を作成し、スキーマとモデルの作成を行ないます。
```javascript:/models/product.js
const mongoose = require("mongoose");

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  price: {
    type: Number,
    required: true,
    min: 0,
  },
  category: {
    type: String,
    enum: ["食品・飲料", "家電", "おもちゃ"],
  },
});

const Product = mongoose.model("Product", productSchema);

module.exports = Product;
```
index.jsからProductモデルを呼び出しておきます。
```javascript:index.js
const Product = require("./models/product")
```

## モックデータ作成（スキップ可）
```javascript:seeds.js
const mongoose = require("mongoose");
const Product = require("./models/product");

main().catch((err) => console.log(err));

async function main() {
  await mongoose.connect("mongodb://127.0.0.1:27017/productApp");
  console.log("db connected");
}
//　商品を1つずつ追加
const p = new Product({
    name: "お茶",
    price: 160,
    category: "食品・飲料",
  });
p.save()

// またはまとめて追加
const products = [
  {
    name: "エアコン",
    price: 29800,
    category: "家電",
  },
  {
    name: "ポケモン",
    price: 3980,
    category: "おもちゃ",
  }
  ...
];
Product.insertMany(products)

```
nodeでseeds.jsを実行するとサンプルデータがインサートされます。
```zsh
node seeds.js
```

# 一覧ページの作成
index.jsでデータの取得とルーティングを行ないます
```javascript:index.js
app.get("/products", async (req, res) => {
  const products = await Product.find({});
  res.render("products/index", { products });
});
```
一覧ページのテンプレートを作成します。
```html:/views/products/index.ejs
<ul>
    <% for (let product of products) { %>
        <li>
            <a href='/products/<%= product._id %>'>
                <%= product.name %>
            </a>
        </li>
    <% } %>
</ul>
```

# 商品追加ページと追加処理

**追加処理**
リクエストボディを受け取るために、
`app.use(express.urlencoded({ extended: true }));`を設定します。

**ルーティング**
カテゴリーリストを定義して、セレクトボックスに反映できるよう、テンプレートに渡します。

```javascript:index.js
app.use(express.urlencoded({ extended: true }));
const categories = ["食品・飲料", "家電", "おもちゃ"];

// 商品追加ページ
app.get("/products/new", (req, res) => {
  res.render("products/new", { categories });
});

// 商品追加処理
app.post("/products", async (req, res) => {
  const newProduct = new Product(req.body);
  await newProduct.save();
  res.redirect(`/products`);
});
```

```html:/views/products/new.ejs
<form action='/products' method="POST">
    <label for='name'>商品名</label>
    <input type='text' name="name" id="name" placeholder="商品名">
    <label for='price'>価格</label>
    <input type='number' name='price' id="price" placeholder="価格">
    <label for='category'>カテゴリー</label>
    <select name='category' id="category">
      <% for(let category of categories){ %>
        <option value='<%= category %>'>
          <%= category %>
        </option>
        <% } %>
    </select>
    <button>登録</button>
</form>
```

# 詳細ページの作成
`findById()`を使用してparamsから商品を取得。テンプレートにデータを渡します。
```javascript:index.js
app.get("/products/:id", async (req, res) => {
  const { id } = req.params;
  const product = await Product.findById(id);
  res.render("products/product", { product });
});
```
詳細ページに編集ページへのリンクを設置しておきます。
```html:/views/products/product.ejs
<h1>
    <%= product.name %>
</h1>
<ul>
    <li>価格：<%= product.price %>円</li>
    <li>カテゴリー：<%= product.category %></li>
</ul>
<a href='/products'>商品一覧</a>
<a href='/products/<%= product._id %>/edit'>編集</a>
```

# 商品編集ページと更新処理

更新処理にPUTを使用するため、`method-override`をインストールします。

```zsh
npm i method-override
```
`method-override`の設定と編集ページのルーティング、更新処理を追加していきます。
```javascript:index.js
const methodOverride = require("method-override");
app.use(methodOverride("_method"));

// 商品編集ページ
app.get("/products/:id/edit", async (req, res) => {
  const { id } = req.params;
  const product = await Product.findById(id);
  res.render("products/edit", { product, categories });
});

// 商品更新処理
app.put("/products/:id", async (req, res) => {
  const { id } = req.params;
  const product = await Product.findByIdAndUpdate(id, req.body, {
    runValidators: true, // バリデーションを有効にする
    new: true, // newオプションを付与することで更新後のオブジェクトが返される
  });
  res.redirect(`/products/${product._id}`);
});
```
**補足**
`runValidators: true`を設定することで、スキーマの`required`,`min`等のバリデーションが有効になります。`new: true`を設定しないと、更新前のオブジェクトが返ってくるので注意。

編集ページのformのactionに下記のように`?_method=PUT`のクエリを設定します。

```html:/views/products/edit.ejs
<form action='/products/<%= product._id %>?_method=PUT' method="POST">
```

# 削除処理

```javascript:index.js
app.delete("/products/:id", async (req, res) => {
  const { id } = req.params;
  await Product.findByIdAndDelete(id);
  res.redirect("/products");
});
```
商品詳細ページに削除ボタンを追加します。
```html:/views/products/product.ejs
<form action='/products/<%= product._id %>?_method=DELETE' method="POST">
    <button>削除</button>
</form>
```
# 完成

以上、Express+Mongoose+EJSで簡単なCRUD操作が行えるアプリを作成しました。