Node.js で Express 製の REST API を実装するメモ。
Book
というモデルの CRUD 用の 単純な API を実装する。
Contents
開発用の実行環境の構築
環境は Docker
, docker-compose
を使う。それぞれセットアップが完了していること。
プロジェクトのルートを用意。 アプリのソースはsrc
にいれる。
$ mkdir myapi
$ cd myapi
$ mkdir src
$ touch Dockerfile
$ touch docker-compose.yml
Dockerfile
FROM node:11
WORKDIR /home/node
RUN apt-get -y update\
&& npm install -g express\
&& npm install -g express-generator
UESR node
Dockerfile
は、Node をベースにしたものを使う。apt-get update
でパッケージは更新しておく。express
とexpress-generator
は root じゃないと入れられないので入れておく。CMD
はまだ指定しない
docker-compose.yml
version: "3"
services:
web:
build:
context: .
environment:
- TZ=Asia/Tokyo
- http_proxy=${http_proxy}
- https_proxy=${https_proxy}
ports:
- 3000:3000
depends_on:
- mongodb
volumes:
- ./src:/home/node
command: bash
tty: true
mongodb:
image: mongo:4.2.8
volumes:
- mongodata:/data/db
volumes:
mongodata:
- command と tty を指定してコンテナが終了しないようにする。
そして build。proxy が必要な場合は --build-args http_proxy=http://proxy:8080 --build-args https_proxy=http://proxy:8080
をオプション追加する
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec web bash
- DockerImage ビルド
- コンテナ起動
- node のコンテナ接続
Express のインストール
ここからはコンテナ内での作業。
$ express
$ npm install
$ npm start
- express のプロジェクトのひな型を生成し、
- express のデフォルトの依存モジュールをインストール
- express アプリ を起動
express の web アプリが起動したので、http://localhost:3000 にアクセスできるか確認する。
REST API の実装
ひな形から不要なものを削除。public
,views
やroutes/user.js
は削除
ORM ライブラリはmongoose
を使う。
$ npm install mongoose
app.js の修正
最初に自動生成された app.js
は以下のようにする。MongoDB の接続 URI は環境変数MONGO_URI
に指定する
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require("./routes/index");
var app = express();
// mongodb接続
const mongoose = require("mongoose");
mongoose.connect(process.env.MONGO_URI).catch((e) => {
console.log(e);
});
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use("/", indexRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.send("Internal Server Error"); // WebAPIアプリケーションなのでsendにする。
});
module.exports = app;
Book の model
/models/book.js
を用意する。
let mongoose = require("mongoose");
let Schema = mongoose.Schema;
//book schema definition
let BookSchema = new Schema(
{
title: { type: String, required: true },
author: { type: String, required: true },
year: { type: Number, required: true },
pages: { type: Number, required: true, min: 1 },
},
{
timestamps: true,
versionKey: false,
}
);
module.exports = mongoose.model("Book", BookSchema);
Book の Route
/routes/index.js
を編集
var express = require("express");
var router = express.Router();
const bookRouter = require("./book");
router.use("/books", bookRouter);
// リソースが増える場合に追加していけばよい
// router.user('/stores', storeRouter)
module.exports = router;
一覧 GET /books/
Book の一覧を返す
const express = require("express");
const router = express.Router();
const Book = require("../models/book");
/**
* GET /books/
* 一覧
*/
router.get("/", async (req, res, next) => {
try {
const books = await Book.find({});
res.status(200).json(books);
} catch (e) {
next(e);
}
});
module.exports = router;
以降module.exports = router;
をファイルの末尾に記述したままで、各 router を追記してく。
登録 POST /books/
/**
* POST /books/
* 登録
*/
router.post("/", async (req, res, next) => {
try {
let book = new Book(req.body);
// validation
let invalid = book.validateSync();
if (invalid) {
console.log(invalid);
res.status(400).json({ error: invalid.message });
return;
}
await book.save();
res.status(200).json(book);
} catch (e) {
next(e);
}
});
詳細 GET /books/:id
/**
* GET /books/:id
* 詳細
*/
router.get("/:id", async (req, res, next) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
res.status(404).json({ error: "Not Found." });
return;
}
res.status(200).json(book);
} catch (e) {
next(e);
}
});
更新 PATCH /books/:id
/**
* PATCH /books/:id
* 更新
*/
router.patch("/:id", async (req, res, next) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
res.status(404).json({ error: "Not Found." });
return;
}
// フィールドの更新
for (field of ["title", "author", "year", "pages"]) {
if (field in req.body) book[field] = req.body[field];
}
// Validate
let invalid = book.validateSync();
if (invalid) {
console.error(invalid);
res.status(400).json({ error: invalid.message });
return;
}
// save
await book.save();
res.status(200).json(book);
} catch (e) {
next(e);
}
});
削除 DELETE /books/:id
/**
* DELETE /books/:id
*/
router.delete("/:id", async (req, res, next) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
res.status(404).json({ error: "Not Found." });
return;
}
await book.remove();
res.status(204).send();
} catch (e) {
next(e);
}
});
不十分なもの
- ユニットテスト
- 細かいバリデーション
- ロギング
- 認証
ユニットテストをmocha
+chai
+sinon
で実装する
GET /books/:id の :id は ObjectId に準拠してない場合に例外になるので、事前に検証し 404 を返すなど処理が必要
デバッグログを console.log で出力してるので log4js などの logger の機能が必要。
ほとんどの環境は、認証が必要なので、API キーや barer トークンなどで認証する機能が必要