LoginSignup
35
17

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-06

はじめに

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

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などのフレームワークに適応しています。
クラスベースであることにより、構造的かつ綺麗なコードを書くことができます。
例として、以下のようにクラスのメソッドをコントローラーのハンドラーとして書くことができます。

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

パッケージインストール

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

TSCONFIGの設定

tsc --init
# tsconfig.jsonができればOK

以下の3つの設定を変更

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

コントローラーを書いてみる

ポケモンのデータを返す処理を書いてみました。

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のドキュメントをご参照ください。
  • URL内のパラメータを受け取るときは@Paramを使います。

app.ts

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

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();

実行してみる

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

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

Express単体との比較

非同期処理

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

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です。

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

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

バリデーション

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

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では、デコレーターで書くことができます。

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以外のフレームワークにも適用できるので是非お試しください。

35
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
17