はじめに
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などのフレームワークに適応しています。
クラスベースであることにより、構造的かつ綺麗なコードを書くことができます。
例として、以下のようにクラスのメソッドをコントローラーのハンドラーとして書くことができます。
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つの設定を変更
{
"compilerOptions": {
~
"strictPropertyInitialization": false,
/* Experimental Options */
"experimentalDecorators": true,
"emitDecoratorMetadata": true
~
}
}
コントローラーを書いてみる
ポケモンのデータを返す処理を書いてみました。
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番でリッスンしています。
先ほど書いたコントローラーをインポートします。
比較的シンプルに書くことができます。
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でハンドラーを書く際、非同期処理を即時間数でラップするなど面倒な書き方になってしまいます。
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です。
import {
JsonController,
Get,
} from 'routing-controllers';
@JsonController()
export class UserController {
@Get('/users')
async users(): Promise<User[]> {
return getUsers();
}
}
バリデーション
Expressでバリデーションをするときは、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では、デコレーターで書くことができます。
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以外のフレームワークにも適用できるので是非お試しください。