Posted at

年末休みにnodeでAPIサーバを勉強した復習〜part2 単純なAPI作成


フォルダのリファクタリングを行った

server 

 dist

 docker

 node_module

 src

  config

  controllers

  models

  routes

とりあえずこのように変更した。

distにはトランスパイルしたjsが入る

dockerには各Dockerの設定とデータ

node_moduleにはmodule

src以下にTypescriptのソース

configには 各種コンフィグ、Express、mongoose等のライブラリの設定

controllersにはControl部分(細かい機能分けはあとで・・)

modelsにはModel(DBの操作)

routesにはルーティング


ふわっとTypescript対応

https://kuroeveryday.blogspot.com/2018/03/migrating-from-javascript-to-typescript.html

https://www.ikemo3.com/inverted/typescript/javascript-to-typescript/

こんなページを見ながら徐々に移行した

新幹線に乗って実家に帰る途中で作業をした


拡張子変更

tsconfigを最も緩く設定したため

拡張子を .tsにしたら 大体うごいた


import

require構文をimportに変更した

それと同時に exportをTypescriptに合わせた

default exportという概念も覚えた

エラーが大量にでるが 大概は @typesをインストールすればなおった

が、

const JwtStrategy = require('passport-jwt').Strategy;

のようなものがエラーが取れなかったので 後回しにした

const path = require('path');

const express = require('express');
const httpError = require('http-errors');

module.exports = app;

のようなコードが

import path from 'path';

import express from 'express';
import httpError from 'http-errors';

export default app;

のようなイメージである


any

Typescriptはすべての変数に型を書くべきである

stringやnumberは簡単に型を書くことが出来た

が interface的なものや 関数等面倒なものはとりあえず anyでごまかした


トランスパイル

タスクランナーでファイル変更したら自動でトランスパイルするようにしたので

docker-compose up しておけば勝手にトランスパイルしてくれる

非常に便利だ


DBアクセスの簡単なAPIを作る

ExpressとMongooseを設定し 簡単なAPIを作る

APIのエントリポイントを /apiをし、その下にRouteをつくる


express設定

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(compress());
app.use(methodOverride());

app.use('/api', routes);

ミドルウェアは適当に設定してる。とりあえずこれで /apiから始まるリクエストは reoutesへ転送される


mongoose設定

const mongoUri = config.mongo.host;

mongoose.connect(mongoUri, { keepAlive: 1 });
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${mongoUri}`);
});

configの設定を渡し mongoに接続する


model

userテーブルのスキーマを作る

import mongoose from 'mongoose';

import Joi from "Joi";

const schema = new mongoose.Schema({
user_id: {
type: String,
required: true,
unique: true
},
user_name: {
type: String,
required: true
},
memo: {
type: String,
required: false
},
password: {
type: String,
required: true
},
life_max: {
type: Number,
required: true
},
life: {
type: Number,
required: true
},
level: {
type: Number,
required: true
},
exp: {
type: Number,
required: true
},
coin: {
type: Number,
required: true
},
charged_coin: {
type: Number,
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});

export const User = mongoose.model("User", schema);
export default User;

export namespace UserSchema {

export const insertSchema = Joi.object({
user_id: Joi.string().required(),
user_name: Joi.string().required(),
memo: Joi.string(),
password: Joi.string().required(),
life_max: Joi.number().required(),
life: Joi.number().required(),
level: Joi.number().required(),
exp: Joi.number().required(),
coin: Joi.number().required(),
charged_coin: Joi.number().required(),
});
export const updateSchema = Joi.object({
user_name: Joi.string(),
memo: Joi.string()
});
};

Joiというライブラリが非常に優秀だった

値のバリデートが簡単に実装できる

メールアドレスや電話番号等のバリデートも可能だ

今回は insertとupdate時もスキーマを定義し、値のチェックを入れた


router

まず /apiから index.tsにルーティングされるようにした


index.ts

import express from "express";

import userRoutes from './user.route';

const router = express.Router();

router.use('/user/:user_id/user', userRoutes);

export default router;


その後それぞれの機能にルーティングする

ここでは /user機能へルーティングする


user.ts

import { default as express, Response, Request } from "express";

import asyncHandler from 'express-async-handler';
import * as userCtrl from '../controllers/user.controller';

const router = express.Router( { mergeParams: true });

router.route('/status')
.get(asyncHandler(status_read))
.put(asyncHandler(status_insert))
.post(asyncHandler(status_update))
.delete(asyncHandler(status_delete));

async function status_read(req: Request, res: Response) {
let user = await userCtrl.status_read(req);
res.json(user);
}

// for debug
async function status_insert(req: Request, res: Response) {
console.log(req.body);
let user = await userCtrl.status_insert(req);
res.json(user);
}

async function status_update(req: Request, res: Response) {
let user = await userCtrl.status_update(req);
res.json(user);
}

// for debug
async function status_delete(req: Request, res: Response) {
let user = await userCtrl.status_delete(req);
res.json(user);
}

export default router;


今の段階では、RoutesとControllers、Modelsの機能分割を考えてないので後でリファクタリングしたい


Controlelrs

import bcrypt from 'bcrypt';

import User, {UserSchema} from "../models/user.model";
import { Request } from "express";
import Joi from "Joi";
var moment = require('moment-timezone');

export async function status_read(req: Request) {
// TODO: _idを非表示になぜか出来ない
let result = await User.find({ user_id: req.params.user_id }, {_id:0}).exec();
return await new User(result[0]);
}

export async function status_insert(req: Request) {
// TODO: RequestのValidate
req.body["user_id"] = req.params.user_id;
// TODO: Ramdom Password
req.body["password"] = bcrypt.hashSync(req.body.password, 10);

// delete req.body.password;
let val = await Joi.validate(req.body, UserSchema.insertSchema, { abortEarly: true });
return await new User(val).save();
}

export async function status_update(req: Request) {
// TODO: RequestのValidate
let val = await Joi.validate(req.body, UserSchema.updateSchema, { abortEarly: true });
val["updatedAt"] = moment().tz("Asia/Tokyo").format();

// delete req.body["user_id"];
console.log({$set: val });
return await User.updateOne({user_id: req.params.user_id}, {$set: val });
}

export async function status_delete(req: Request) {
return await User.remove({user_id: req.params.user_id});
}


あとがき

Joiはとても便利。Validateが圧倒的に楽になった

bcryptはgipで実装されてるため色々エラーが出て苦労した。bcrypt-nodeというのがありそっち使えばよかったかも?

JavascriptのTimezoneはカオス

async/awaitは説明しないとダメかな

リファクタリングしたい