Edited at

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

More than 1 year has passed since last update.


はじめに

最近は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 = (req:restify.Request, res:restify.Response, next:restify.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ちゃんと学習しないとな。