バックエンド側 node.js バリデーション/環境ファイル/DB

2023-11-04

この備忘録がないと何も思い出せないパクリ18発目!
けど、既存知識の横展開部分が多いため機械学習とかに比べると簡単。

パクリ元

内容

以下一覧を完了させて ECS リリースさせることでバックエンド側を終わりとする。順序は適宜入れ替える。

  • 開発環境構築
  • プロジェクト作成・最適なフォルダ構成
  • (認証・認可) フロントエンドとの連携が必要なので今回省略。
  • 認証チェック(含めた前処理) 
  • ログ処理
  • APIによってはIPチェックやHeaderチェック
  • バリデーション    ←←← 今回ココから。
  • 環境ファイルによる振り分け
  • DB参照・更新
  • API取得
  • API取得待ち合わせ
  • 長時間処理を非同期処理で
  • メール送信
  • CSVアップロード
  • ファイルダウンロード
  • EXCEL/PDF作成
  • テスト
  • AWS環境へデプロイ

nodemon再修正

ハマった。前回の nodemon 指定だと全ファイルを監視していたため処理途中にたぶんログやらが更新されたことでトランザクションが終わる前にリスタートされ、DBの更新がままならなかった。ts ファイルだけ監視に。いやハマった、、

~/nodetest/package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon src/index.ts --ext 'ts'"
  },

バリデーション

バリデーションを行おうとするとルーティングとコントローラーをキッチリ分けた方が分かりやすいので、まずはパクリ元1つ目を参考にフォルダ構成を変更する。

~/nodetest/src/routes/index.ts
import express from 'express'
import userRoutes from './userRoute'
const router = express.Router()

router.use('/user', userRoutes)
// 404
router.all("*", (req, res) => {
  res.send(404)
})
export default router
~/nodetest/src/routes/userRoute.ts
import express from 'express';
import type { Express, Request, Response, NextFunction } from 'express';
import {authenticate} from '../middleware/authenticate';
const router = express.Router();

router.get("/", [authenticate], (req:Request, res:Response) => {
  res.json([{ id: 1 }]);
});
export default router;
~/nodetest/src/index.ts
import router from './routes';
・・・
app.use('/',  router);

ルーティングをまとめることができてスッキリ。

ここでパクリ元2つ目3つ目を参考にバリデーションに対応させ、正常な場合にコントローラーを実行する。

~/nodetest$ npm install --save express-validator
~/nodetest$ npm install --save-dev @types/express-validator
~/nodetest/src/validators/userUpdateValidator.ts
import type { Request, Response, NextFunction } from 'express';
import { check, validationResult } from 'express-validator';

export const userUpdateValidator = [
    check('name')
    .isLength({ min: 6, max: 10})
    .withMessage('名前は6文字以上10文字以内で入力してください')
    .isAlphanumeric()
    .withMessage('名前は英数字で入力してください'),
    (req:Request, res:Response, next:NextFunction) => {
    const errors = validationResult(req);
    if (!errors.isEmpty())
        return res.status(422).json({errors: errors.array()});
    next();
    },
];
~/nodetest/src/routes/userRoute.ts
import express from 'express';
import {authenticate} from '../middleware/authenticate';
import {userUpdateValidator} from '../validators/userUpdateValidator'
import userController from '../controllers/userController'
const router = express.Router();

router.get("/list",
  authenticate,
  userController.list
);
router.post("/update", 
  authenticate,
  userUpdateValidator,
  userController.update, 
);
export default router;
~/nodetest/src/controllers/userController.ts
import type { Express, Request, Response, NextFunction } from 'express';

export default {
    list: (req: Request, res: Response) => {
        res.json([{ id: 1 }]);
    },
    update: (req: Request, res: Response) => {
        res.json({ result: "success", name: req.body.name });
    },
}

curl で試してみる。

~/nodetest$ curl localhost:3001/user/list
{"result":"failure","message":"authentication error"}
~/nodetest$ curl -b "id=a" localhost:3001/user/list
[{"id":1}]
~/nodetest$ curl -X POST -H "Content-Type: application/json" -d '{"name" : "***"}' -b "id=a" localhost:3001/user/update
{"errors":[{"type":"field","value":"***","msg":"名前は6文字以上10文字以内で入力してください","path":"name","location":"body"},{"type":"field","value":"***","msg":"名前は英数字で入力してください","path":"name","location":"body"}]}
~/nodetest$ curl -X POST -H "Content-Type: application/json" -d '{"name" : "test11"}' -b "id=a" localhost:3001/user/update
{"result":"success","name":"test11"}

環境ファイルによる振り分け

DB使用の前に、パクリ元4つ目を参考に環境依存変数を環境ファイルから読み込むようにする。とりあえずSQLite で試すが本番では違う DB もしくは RDS 経由かもしれんから。開発時に .env.development/.env.test/.env.production を用意しておいてデプロイ時に .env にリネームする。

~/nodetest$ npm install --save dotenv
~/nodetest/.env.development・~/nodetest/.env
NODE_ENV=development
PORT=3001
~/nodetest/src/index.ts
import 'dotenv/config'
・・・
const port = process.env.PORT || 3001;
const environment = process.env.NODE_ENV || 'development';
app.listen(port, () => console.log(`environment:${env} / listening on port:${port}!`));

DB参照・更新

テーブル作成

パクリ元5つ目を参考に開発環境用の sqlite と Prisma という ORM を準備する。

~/nodetest$ npm install --save-dev prisma
~/nodetest$ npx prisma init --datasource-provider sqlite
~/nodetest/prisma/schema.prisma
・・・
model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
}
~/nodetest$ npx prisma migrate dev
・・・
✔ Enter a name for the new migration: … create_user
Applying migration `20231024005703_create_user`
・・・
~/nodetest/.env.development・~/nodetest/.env
NODE_ENV=development
PORT=3001

DATABASE_URL="file:./dev.db"

マイグレーションOK、理解した。データ投入もやってみる。

データ投入

~/nodetest/prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  const alice = await prisma.user.upsert({
    where: { email: 'alice@prisma.io' },
    update: {},
    create: {
      email: 'alice@prisma.io',
      name: 'Alice',
    },
  });

  console.log({ alice });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
~/nodetest/package.json
・・・
"prisma": {
    "seed": "ts-node prisma/seed.ts"
},
・・・
~/nodetest$ npx prisma db seed

データ投入まで理解。本来ならテーブルごとの Seeder を作るところだろうな。

データ確認

~/nodetest$ npx prisma studio

PhpMyAdmin のようなツールが立ち上がる。OK牧場!

プログラムからのDBデータ取得

次はプログラム側から呼ぶようにする。コントローラーまでしか作ってないのでモデルまで作る。

~/nodetest/src/routes/userRoute.ts
import express from 'express';
import {authenticate} from '../middleware/authenticate';
import {userUpdateValidator} from '../validators/userUpdateValidator'
import userController from '../controllers/userController'
const router = express.Router();

router.get("/list",
  authenticate,
  userController.list
);
router.post("/insert", 
  authenticate,
  userUpdateValidator,
  userController.insert, 
);
router.post("/update", 
  authenticate,
  userUpdateValidator,
  userController.update, 
);
export default router;
~/nodetest/src/validators/userUpdateValidator.ts
import type { Request, Response, NextFunction } from 'express';
import { check, validationResult } from 'express-validator';

export const userUpdateValidator = [
    check('name')
    .isLength({ min: 6, max: 10})
    .withMessage('名前は6文字以上10文字以内で入力してください'),
    check('email')
    .isEmail()
    .withMessage('メールアドレスを正しく入力してください'),
    (req:Request, res:Response, next:NextFunction) => {
    const errors = validationResult(req);
    if (!errors.isEmpty())
        return res.status(422).json({errors: errors.array()});
    next();
    },
];
~/nodetest/src/controllers/userController.ts
import type { Express, Request, Response, NextFunction } from 'express';
import userModel from '../models/userModel'

export default {
    list: async(req: Request, res: Response) => {
        res.json(await userModel.getList());
    },
    insert: async(req: Request, res: Response) => {
        const { name, email } = req.body;
        res.json(await userModel.insert(name, email));
    },
    update: async(req: Request, res: Response) => {
        const { id, name, email } = req.body;
        res.json(await userModel.update(id, name, email));
    },
}
~/nodetest/src/models/userModel.ts
import { Prisma, PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
    log: ['query', 'info', 'warn', 'error'],
})

export default {
    getList: () => {
        return prisma.user.findMany()
        .catch((e) => {
          throw e
        })
        .finally(() => {
           prisma.$disconnect()
        })
    },
    insert: (_name: string, _email: string) => {
        const main = async () => {
            const user = await prisma.user.create({
                data: {
                    name: _name,
                    email: _email,
                },
            });
            return user;
        }
        return main()
        .catch((e) => {
          throw e
        })
        .finally(() => {
           prisma.$disconnect()
        })
    },
    update: (_id: number, _name: string, _email: string) => {
        const main = async () => {
            const user = await prisma.user.update({
                where: {
                    id: _id,
                },
                data: {
                    name: _name,
                    email: _email,
                },
            });
            return user;
        }
        return main()
        .catch((e) => {
          throw e
        })
        .finally(() => {
           prisma.$disconnect()
        })
    },
}

キレイに機能を分割できた気がする。けど、、async/await の置き場がいまいちよく分からん。DB 取得/更新時に必要かと思っていたのだが、もう一つ上側のコントローラー側につけないとデータが取れなかったり。いずれ分かるだろう。

現状のフォルダ構成

nodetest
┣━ config
┃  ┗━ log4js.json
┣━ dist
┣━ log
┃  ┣━ access.yyyymmdd.log
┃  ┗━ app.yyyymmdd.log
┣━ node_modules
┣━ prisma
┃  ┣━ migrations
┃  ┃   ┣━ yyyymmddhhmiss_create_user
┃  ┃   ┃   ┗━ migration.sql
┃  ┃   ┗━ migration_lock.toml
┃  ┣━ dev.db
┃  ┣━ schema.prisma
┃  ┗━ seed.ts
┣━ src
┃  ┣━ controllers
┃  ┃   ┗━ userController.ts
┃  ┣━ middleware
┃  ┃   ┣━ addResponseHeader.ts
┃  ┃   ┣━ authenticate.ts
┃  ┃   ┣━ checkIpHeader.ts
┃  ┃   ┗━ errorHandler.ts
┃  ┣━ models
┃  ┃   ┗━ userModels.ts
┃  ┣━ routes
┃  ┃   ┣━ index.ts
┃  ┃   ┗━ userRoute.ts
┃  ┣━ types
┃  ┃   ┣━ authenticationError.ts
┃  ┃   ┗━ error.ts
┃  ┣━ validators
┃  ┃   ┗━ userUpdateValidator.ts
┃  ┗━ index.ts
┣━ .env
┣━ .env.development
┣━ .gitignore
┣━ package-lock.json
┣━ package.json
┗━ tsconfig.json

雑感

先はまだまだ長い。一度ハマったりすると時間がものすごくムダになる。