Node.jsでも綺麗なコードでWebAPIを作る（routing-controllers）

# はじめに
Node.jsでWebAPIを作ると、その自由度の高さからコードが綺麗に書けないことが多いと思います。
そんなときにはrouting-controllersを使うのがおすすめです。
今回はrouting-controllersを使ったモダンなWebAPIの書き方を紹介します。

## routing-controllersとは
https://github.com/typestack/routing-controllers
>Allows to create controller classes with methods as actions that handle requests. You can use routing-controllers with express.js or koa.js.

いわゆるMVCのコントローラーをTypescriptのクラスベースで書くことができるライブラリで、express.jsやkoa.jsなどのフレームワークに適応しています。
クラスベースであることにより、構造的かつ綺麗なコードを書くことができます。
例として、以下のようにクラスのメソッドをコントローラーのハンドラーとして書くことができます。

```{typescript:sampleController.ts}
import { Controller, Param, Body, Get, Post, Put, Delete } from "routing-controllers";

@Controller()
export class UserController {

    @Get("/users")
    getAll() {
       return "This action returns all users";
    }

    @Get("/users/:id")
    getOne(@Param("id") id: number) {
       return "This action returns user #" + id;
    }

    @Post("/users")
    post(@Body() user: any) {
       return "Saving user...";
    }

    @Put("/users/:id")
    put(@Param("id") id: number, @Body() user: any) {
       return "Updating a user...";
    }

    @Delete("/users/:id")
    remove(@Param("id") id: number) {
       return "Removing user...";
    }
}
```

Typescriptに馴染みのない人には見慣れない構文があるかと思います。
`@Get("/users")`などはTypescriptのデコレーターという機能になります。
https://www.typescriptlang.org/docs/handbook/decorators.html
デコレータとはクラスの宣言などに（ここではメソッドに対して）アタッチできる特別な宣言です。

## さっそく作ってみる
こちらに詳細なコードが載っています。
https://github.com/tonio0720/modernApiInTypescript

### パッケージインストール

```{sh}
npm init # 初期化
npm i -S express reflect-metadata routing-controllers class-transformer class-validator
npm i -D @types/express ts-node
```

### TSCONFIGの設定

```{sh}
tsc --init
# tsconfig.jsonができればOK
```

以下の3つの設定を変更

```{json:tsconfig.json}
{
    "compilerOptions": {
        ~
        "strictPropertyInitialization": false,
        /* Experimental Options */
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
        ~
    }
}
```

### コントローラーを書いてみる
ポケモンのデータを返す処理を書いてみました。

```{typescript:controllers/PokemonController.ts}
import {
    JsonController,
    Get,
    QueryParams,
    Param,
} from 'routing-controllers';
import { IsInt, IsOptional } from 'class-validator';

interface Pokemon {
    id: number;
    name: string;
    type1: string;
    type2: string;
}

const pokemons: Pokemon[] = [
    {
        id: 1,
        name: 'フシギダネ',
        type1: 'くさ',
        type2: 'どく'
    },
    {
        id: 2,
        name: 'フシギソウ',
        type1: 'くさ',
        type2: 'どく'
    },
    {
        id: 3,
        name: 'フシギバナ',
        type1: 'くさ',
        type2: 'どく'
    }
]

class GetPokemonQuery {
    @IsInt()
    @IsOptional()
    limit?: number;

    @IsInt()
    @IsOptional()
    offset?: number;
}

@JsonController()
export class PokemonController {
    @Get('/pokemons')
    async pokemons(
        @QueryParams() query: GetPokemonQuery
    ): Promise<Pokemon[]> {
        const { offset = 0, limit = 100 } = query;
        return pokemons.slice(offset, offset + limit);
    }

    @Get('/pokemon/:id')
    async pokemon(
        @Param('id') id: number
    ): Promise<Pokemon> {
        const pokemon = pokemons.find((pokemon) => pokemon.id === id);
        if (pokemon) {
            return pokemon;
        }
        throw new Error('no pokemon');
    }
}
```

#### 解説
* クラスに対して`@JsonController`デコレーターを付けることでレスポンスをJSONとして扱うことを意味します。
* リクエストのメソッドがGETのときは`@Get`、POSTのときは`@Post`という風にデコレーターを付与します。
* クエリパラメータを受け取るときは、`@QueryParams`を使います。
    - クエリパラメータもクラスベースで書くことができます。
    - `@IsInt`を付けることによって、バリデートやサニタイズを自動でしてくれます。
    - 他にも`@IsBoolean`、`@IsPositive`などが使えます。
    - 詳しくは[class-validator](https://github.com/typestack/class-validator)のドキュメントをご参照ください。
* URL内のパラメータを受け取るときは`@Param`を使います。

### app.ts
`app.ts`がメインファイルになります。
Expressサーバーを起動し、ポート3000番でリッスンしています。
先ほど書いたコントローラーをインポートします。
比較的シンプルに書くことができます。

```{typescript:app.ts}
import 'reflect-metadata';
import express from 'express';
import bodyParser from 'body-parser';
import {
    useExpressServer
} from 'routing-controllers';
import { PokemonController } from './controllers';

const PORT = 3000;

async function bootstrap() {
    const app = express();

    app.use(bodyParser.json());

    useExpressServer(app, {
        controllers: [
            PokemonController
        ]
    });

    app.listen(PORT, () => {
        console.log(`Express server listening on port ${PORT}`);
    });
}

bootstrap();
```

### 実行してみる

```{sh}
ts-node app.ts
# 「Express server listening on port 3000」となれば成功
```

ブラウザなどから、
http://localhost:3000/pokemons?limit=1
にアクセスしてレスポンスが返れば成功です。

## Express単体との比較
### 非同期処理
Expressでハンドラーを書く際、非同期処理を即時間数でラップするなど面倒な書き方になってしまいます。

```{javascript:expressの場合}
const express = require('express');
const router = express.Router();

router.get('/users', (req, res, next) => {
    (async () => {
        const users = await getUsers();
        res.status(200).json(users);
    })().catch(next);
});
```

一方でrouting-controllersでは、Promise型をそのまま返すだけでOKです。

```{typescript:rcの場合}
import {
    JsonController,
    Get,
} from 'routing-controllers';

@JsonController()
export class UserController {
    @Get('/users')
    async users(): Promise<User[]> {
        return getUsers();
    }
}
```

### バリデーション
Expressでバリデーションをするときは、express-validatorを使います。

```{javascript:express-validatorの場合}
const { body, validationResult } = require('express-validator');

app.post('/user', [
    body('username').isEmail(),
    body('password').isLength({ min: 6 })
], (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    // ...
});
```

routing-controllersでは、デコレーターで書くことができます。

```{typescript:rcの場合}
export class User {
    @IsEmail()
    email: string;

    @MinLength(6)
    password: string;
}

@JsonController()
export class UserController {
    @Post('/login')
    async login(
        @Body() user: User
    ) {
        // ...
    }
}
```

## おわりに
いかがでしたでしょうか？
routing-controllersを使うことで、バリデーションやサニタイズもしつつ綺麗にコードを書くことができました。
routing-controllersはexpress.js以外のフレームワークにも適用できるので是非お試しください。