Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What are the problem?
Organization

express+TypeScriptでAPIを構築する際にやったこと

はじめに

今まではPHPでAPIを構築することが多かったが、メンバーのスキルセットがjs寄りだったため、
express(node.js)でAPIを構築することになったので、その際のメモ。

環境

  • Windows10
  • Docker for Windows:3.0.0
  • node.js:v14.15.1
  • npm:6.14.8
  • TypeScript:4.1.3
  • express:4.16.1
  • MySQL:8.0.21
  • eslint:7.15.0
  • prettier:2.2.1

Dockerで動作環境を構築

docker-compose.ymlはこんな感じ

docker-compose.yml
version: "3"
services:
  mysql:
    image: mysql:8
    command: --default-authentication-plugin=mysql_native_password
    env_file: ./mysql/.env
    ports:
      - 3306:3306
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/conf/my.cnf:/etc/my.cnf
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    env_file: ./phpmyadmin/.env
    ports:
      - 8080:80
  app:
    image: node:12
    env_file: ../api-dir/.env # Docker用のディレクトリと同列にAPIのディレクトリを配置
    tty: true
    ports:
      - 3000:3000
    volumes:
      - ../api-dir:/app
    working_dir: /app
    command: npm start
volumes:
  mysqldata:

expressの用意

まずDocker用のディレクトリと同列にAPIのディレクトリを作成する

$ cd ../
$ mkdir api-dir
$ cd api-dir

必要なモジュールをnpmでインストール(主なもの)

  • express:node.jsのフレームワーク
  • express-validator:バリデーション
  • knex:クエリビルダー、マイグレーション、シーディング等
  • winston:ログ
  • jest:UnitTest
  • supertest:jestでのAPIリクエストテスト用
  • json2csv:csv変換
  • nodemon:コード変換時の自動再起動
  • ts-node:TypeScriptのトランスパイル
  • eslint:静的解析
  • prettier:コード整形

node.jsのクエリビルダーで良いものがないか探し回った末、knexにたどり着いた。
Laravelに慣れていたが、違和感なく使えた。
公式ドキュメントも充実しているのでオススメ。

各種設定ファイルの作成

package.json(一部抜粋)
"scripts": {
    "start": "NODE_ENV=development npx nodemon --exects-node", // 起動コマンドを変更
    "lint:fix": "eslint */**/*.ts *.ts *.json --fix",
    "db:down": "knex --knexfile knexfile.ts migrate:rollback --all --env test",
    "db:migrate": "knex --knexfile knexfile.ts migrate:latest --env test",
    "db:seed": "knex --knexfile knexfile.ts seed:run --env test",
    "db": "npm run db:down && npm run db:migrate && npm run db:seed",
    "test": "jest --config jestconfig.json --coverage --verbose --runInBand --detectOpenHandles",
}
nodemon.json
{
  "watch": ["src", "seeds", "migrations", "tests"],
  "ext": "ts",
  "exec": "ts-node ./bin/www"
}

jestconfig.json
{
  "testMatch": ["**/?(*.)+(spec|test).+(ts|tsx|js)"],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
  "setupFilesAfterEnv": ["./jest.setup.js"] // タイムアウトの秒数を調整
}
jest.setup.js
jest.setTimeout(5000)
knexfile.ts
if (!require.cache['dotenv']) {
  require('dotenv').config()
}

module.exports = {
  development: { // 開発用
    client: 'mysql',
    connection: {
      host: process.env.MYSQL_SERVER,
      user: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      port: process.env.MYSQL_PORT,
    },
    migrations: {
      extension: 'ts',
      tableName: 'knex_migrations',
    },
  },

  test: { // Jest用
    client: 'mysql',
    connection: {
      host: '127.0.0.1',
      user: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASSWORD,
      database: process.env.MYSQL_DATABASE,
      port: process.env.MYSQL_PORT,
    },
    migrations: {
      extension: 'ts',
      tableName: 'knex_migrations',
    },
  },
}
winston.ts(おまけ)
import fs from 'fs'
import path from 'path'
import moment from 'moment'
const winston = require('winston')
require('winston-daily-rotate-file')

const logDirectory = path.join(__dirname, './logs')
//指定したディレクトリが存在するか?
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory)

const transport = new winston.transports.DailyRotateFile({
  filename: path.join(logDirectory, './error-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
})

transport.on('rotate', function (oldFilename, newFilename) {
  // do something fun
})

// creates a new Winston Logger
const logger = new winston.createLogger({
  level: 'info',
  transports: [transport],
  exitOnError: false,
})

logger.errorLog = (req, res, err) => {
  logger.error(
    `[${moment().format('YYYY-MM-DD HH:mm:ss')}] ${req.method} - ${
      res.statusCode
    } - ${req.originalUrl} - ${req.ip} - ${err.message}`,
  )
}
module.exports = logger
eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "plugin:prettier/recommended"
  ],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/no-var-requires": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "prettier/prettier": [
      "error",
      {
        "trailingComma": "all",
        "endOfLine": "lf",
        "semi": false,
        "singleQuote": true,
        "printWidth": 80,
        "tabWidth": 2
      }
    ]
  }
}

起動

expressの起動

$ cd docker-dir
$ docker-compose up -d // コンテナ起動時にnpm startが実行され、nodemonで変更監視される

テストの際

$ cd docker-dir
$ docker-compose up mysql phpmyadmin -d
$ cd ../api-dir
$ npm run db // DBのセットアップ
$ npm run test

終わりに

今回、フロントエンドはVue.js(TypeScript)、APIはexpress(TypeScript)を採用した結果、
フロントエンジニアにもAPI製造を手伝ってもらうことができた。
言語の壁はあまり感じることなく、スムーズに作業を進められている様子だった。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?