Help us understand the problem. What is going on with this article?

restify + TypeScript + Jestの開発環境をセットアップしてみる

はじめに

最近はTypeScript+ExpressでWeb APIサーバーぼちぼちやってるんだけど、restifyなるExpress拡張がなんかいい感じっぽいので今回はrestify + TypeScriptでRest API Serverを開発してみることにした。テストは最近気に入ってるJestのTypeScript用 ts-jest を使う。

セットアップ

Get started Express + TypeScript する が大変参考になった。restifyとexpressを読み替えればいい感じの Webpack なトランスパイル環境が整えられる。感謝。

以下、コマンドと設定を垂れ流し。

モジュールをインストールする

globalに TypeScriptやWebpackが入ってない場合は入れておく

npm i -g typescript webpack webpack-cli

プロジェクトルートを作成して必要なモジュールをインストールする

mkdir restify-typescript-example && cd $_
mkdir -p src/__tests__
npm init -f

npm i --save restify node-fetch

npm i --save-dev typescript ts-loader tslint tslint-loader tslint-config-airbnb jest ts-jest @types/restify @types/jest @types/node-fetch

npm i --save-dev webpack webpack-cli webpack-node-externals

webpack.configを設定する

上記記事を参考に webpack.config.dev.js webpack.config.prod.jsをプロジェクトルートに作成する

webpack.config.dev.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  target: 'node',               // Module not found: Error: Can't resolve 'fs' 対策
  externals: [nodeExternals()], 
  devtool: 'inline-source-map',
  module: {
    rules: [
        {
          enforce: 'pre',
          loader: 'tslint-loader',
          test: /\.ts$/,
          exclude: [
            /node_modules/,
            "**/*.test.ts" 
          ],
          options: {
            emitErrors: true
          }
        },
        {
          loader: 'ts-loader',
          test: /\.ts$/,
          exclude: [
            /node_modules/,
            "**/*.test.ts" 
          ],
          options: {
            configFile: 'tsconfig.dev.json'
          }
        }
    ]
  },
  resolve: {
    extensions: [ '.ts', '.js' ]
  },
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist')
  }
};
webpack.config.prod.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  target: 'node',               // Module not found: Error: Can't resolve 'fs' 対策
  externals: [nodeExternals()], 
  module: {
    rules: [
      {
        enforce: 'pre',
        loader: 'tslint-loader',
        test: /\.ts$/,
        exclude: [
          /node_modules/,
          "**/*.test.ts" 
        ],
        options: {
          emitErrors: true
        }
      },
      {
        loader: 'ts-loader',
        test: /\.ts$/,
        exclude: [
          /node_modules/,
          "**/*.test.ts" 
        ],
        options: {
          configFile: 'tsconfig.prod.json'
        }
      }
    ]
  },
  resolve: {
    extensions: [ '.ts', '.js' ]
  },
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist')
  }
};

テストファイルを無視するため exclude"**/*.test.ts"した

tsconfig を設定する

tsconfig.dev.json
{
  "compilerOptions": {
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "lib": ["es2018", "dom"],
    "moduleResolution": "node",
    "removeComments": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "strictFunctionTypes": false
  }
}
tsconfig.prod.json
{
  "compilerOptions": {
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "lib": ["es2018", "dom"],
    "moduleResolution": "node",
    "removeComments": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "strictFunctionTypes": false
  }
}

記事にはないが tsconfig.jest.json を追加した。こいつでts-jest実行時のみコンパイルオプション module: "commonjs"を与えてやらないと ERROR: 'import' and 'export' may appear only with 'sourceType: "module"' でテストがコケる

https://github.com/kulshekhar/ts-jest#tsconfig

tsconfig.jest.json
{
  "extends": "./tsconfig.dev.json",
  "compilerOptions": {
    "module": "commonjs"
  }
}

tslintを設定する

tslint.json
{
  "extends": "tslint-config-airbnb",
  "rules": {
    "ter-indent": [true, 2],
    "no-boolean-literal-compare": false 
  }
}

Lintはtslint-config-airbnb
no-boolean-literal-compare: falseにした。ビルド時に警告が出てうざいので

package.jsonを設定する

package.json
{
  "name": "restify-typescript-example",
  "version": "0.1.0",
  "description": "Restify TypeScript Example",
  "main": "index.js",
  "scripts": {
    "start": "node dist/index.js",
    "test": "jest"
  },
  "jest": {
    "globals": {
      "ts-jest": {
        "tsConfigFile": "tsconfig.jest.json"
      }
    },
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "testURL": "http://localhost/",
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts?|jsx?|tsx?)$",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx",
      "json",
      "node"
    ],
    "collectCoverage": true,
    "collectCoverageFrom": [
      "src/**/*.ts"
    ]
  },
  "keywords": [],
  "author": "bathtimefish",
  "license": "MIT",
  "dependencies": {
    "node-fetch": "^2.2.0",
    "restify": "^7.2.1"
  },
  "devDependencies": {
    "@types/jest": "^23.3.1",
    "@types/node-fetch": "^2.1.2",
    "@types/restify": "^7.2.3",
    "jest": "^23.4.2",
    "ts-jest": "^23.1.2",
    "ts-loader": "^4.4.2",
    "tslint": "^5.11.0",
    "tslint-config-airbnb": "^5.9.2",
    "tslint-loader": "^3.6.0",
    "typescript": "^3.0.1",
    "webpack": "^4.16.4",
    "webpack-cli": "^3.1.0",
    "webpack-node-externals": "^1.7.2"
  }
}

とりあえず全部載せとくけど、編集したのは scriptsjest セクションのみ。jest.testURL: "http://localhost/" を設定しないと "SecurityError: localStorage is not available for opaque origins" などと吐かれてテストがコケる。これはハマった。以下の記事に救われました。ありがとうございます。

jestで"SecurityError: localStorage is not available for opaque origins"と言われてテストがコケる

API Serverのサンプルコードを書く

src/index.ts を以下のようにする。Quick Startのecho serverをTSで書いただけ。

index.ts
import * as restify from 'restify';

const respond: restify.RequestHandlerType = (req, res, next) => {
  res.send('hello ' + req.params.name);
  next();
};

export const server = restify.createServer({
  name: 'Restify TypeScript Echo Server',
  version: '0.1.0',
});

server.get('/hello/:name', respond);
server.head('/hello/:name', respond);

server.listen(8080, () => {
  console.log('%s listening at %s', server.name, server.url);
});

テストコードを書く

src/__tests__/index.test.ts を以下のようにする。簡単なテスト

index.test.ts
import * as index from '../index';
import fetch from 'node-fetch';

describe('Index', () => {

  const server = index.server;

  afterAll(() => {
    console.log('afterAll: server.close()');
    server.close();
  });

  describe('server name', () => {
    it('should be valid server name', () => {
      const serverName = 'Restify TypeScript Echo Server';
      expect(server.name).toBe(serverName);
    });
  });

  describe('GET hello', () => {

    it('should be greet', async() => {
      const url = 'http://localhost:8080/hello/';
      const name = 'bathtimefish';
      const res = await fetch(url + name, {method: 'GET'});
      const json = await res.json();
      expect(json).toBe('hello ' + name);
    });

    it('content-type should be text/plain', async() => {
      const url = 'http://localhost:8080/hello/';
      const name = 'bathtimefish';
      const res = await fetch(url + name, {
        method: 'GET',
        headers: {'accept': 'text/plain'}
      });
      const contentType = res.headers.get('content-type');
      expect(contentType).toBe('text/plain');
      const json = await res.text();
      expect(json).toBe('hello ' + name);
    });

  });

  describe('HEAD hello', () => {
    it('connection should be close', async() => {
      const url = 'http://localhost:8080/hello/';
      const name = 'bathtimefish';
      const res = await fetch(url + name, {
        method: 'HEAD',
        headers: {'connection': 'close'}
      });
      const connection = res.headers.get('connection');
      expect(connection).toBe('close');
    });
  });

});

Testを実行する

npm test

> restify-typescript-example@0.1.0 test /Users/user/work/restify-typescript-example
> jest

 PASS  src/__tests__/index.test.ts
  Index
    server name
      ✓ should be valid server name (7ms)
    GET hello
      ✓ should be greet (63ms)
      ✓ content-type should be text/plain (6ms)
    HEAD hello
      ✓ connection should be close (4ms)

  console.log src/index.ts:17
    Restify TypeScript Echo Server listening at http://[::]:8080

  console.log src/__tests__/index.test.ts:9
    afterAll: server.close()

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.ts |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        4.718s
Ran all test suites.

とおった。

開発用にビルドする

Webpackでトランスパイルする。正常に完了すると dist/index.js ができる。

webpack --config webpack.config.dev.js

Hash: e19b805b0d985055c5b0
Version: webpack 4.16.4
Time: 3107ms
Built at: 2018-08-04 16:56:52
   Asset    Size  Chunks             Chunk Names
index.js  11 KiB    main  [emitted]  main
Entrypoint main = index.js
[./src/index.ts] 425 bytes {main} [built]
[restify] external "restify" 42 bytes {main} [built]

リリース用ビルドの場合は webpack --config webpack.config.prod.js を実行する。

開発用サーバを実行する

npm start

> restify-typescript-example@0.1.0 start /Users/user/work/restify-typescript-example
> node dist/index.js

Restify TypeScript Echo Server listening at http://[::]:8080

Quick Start に載ってるようにリクエストしてみる

curl -is http://localhost:8080/hello/bathtimefish -H 'accept: text/plain'

HTTP/1.1 200 OK
Server: Restify TypeScript Echo Server
Content-Type: text/plain
Content-Length: 18
Date: Sat, 04 Aug 2018 08:00:53 GMT
Connection: keep-alive

hello bathtimefish
curl -is http://localhost:8080/hello/bathtimefish

HTTP/1.1 200 OK
Server: Restify TypeScript Echo Server
Content-Type: application/json
Content-Length: 20
Date: Sat, 04 Aug 2018 08:02:10 GMT
Connection: keep-alive

"hello bathtimefish"
curl -is http://localhost:8080/hello/bathtimefish -X HEAD -H 'connection: close'

HTTP/1.1 200 OK
Server: Restify TypeScript Echo Server
Date: Sat, 04 Aug 2018 08:03:15 GMT
Connection: close

できた。

まとめ

全ソースコードはこちら

https://github.com/bathtimefish/restify-typescript-example

いい感じに整った気がする。細かいワークフローの自動化はここから適宜改造していけばいいと思う。

ちなみに、VSCodeでvscode-jest入れてるとテストコード書くごとに自動でTestRunnerが走ってpass/failが可視化されてめっちゃ便利。そろそろ本格的にVSCodeに乗り換えるべきかなと思う。

スクリーンショット 2018-08-04 17.12.31.png

さて、restifyちゃんと学習しないとな。

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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした