日本語情報が全く見当たらないExpressのTypeScript decoratorであるOvernightJSを使ってみたので、導入方法をざっくりまとめます。
Node/ExpressでTypeScriptを使う方法については、簡単に書きはしますが本筋ではないので詳細は他を当たってください。このあたりとか参考になります。
前提環境
- Node 12.18.4
- Express 4.17.1
- TypeScript 4.0.3
開発環境でのTypeScriptの実行にはts-nodeとts-node-devを使います。ts-node-devはコード修正するとホットリロードしてくれるのでとても楽。
セットアップ
$ npm init
プロジェクトの設定をよしなに
$ npm install express @overnightjs/core @overnightjs/logger body-parser cors http-status-codes
$ npm install -D typescript npm-run-all rimraf ts-node ts-node-dev @types/node@12 @types/express @types/cors
$ npx tsc --init
tsconfig.jsonが生成されるので、その中にあるtarget
をES2019
に、esModuleInterop
とexperimentalDecorators
とemitDecoratorMetadata
のコメントを外してtrueに設定します。
package.jsonのscripts
部分はこんな感じにしました。ここはOvernightJS固有の要素は特になくTypeScript使う場合の一般的な設定内容です。開発環境ではts-nodeやts-node-devでTypeScriptファイルを直接実行、本番環境ではトランスパイルします。
"scripts": {
"dev": "ts-node ./src/index.ts",
"dev:watch": "ts-node-dev ./src/index.ts",
"build": "npm-run-all clean tsc",
"clean": "rimraf dist/*",
"tsc": "tsc",
"start": "node ."
},
余談ですがトランスパイルして実行するのとts-nodeで直接実行するのでパフォーマンス大差ないという話もあるので、要件がシビアでなければ本番もts-node運用でもいいかもしれません。
実装
大雑把にコントローラーとサーバーの二段構成です。型安全過激派なので徹底的に型で縛ります。
コントローラー
@Controller
というアノテーションを付けたクラスがコントローラーと認識されます。パラメーターはエンドポイントのパスで、サーバーがlocalhostの3000番ポートで動いているとすると以下の例ではhttp://localhost:3000/api/songs
がエンドポイントになります。
各HTTPメソッドに対応するアノテーション@Get
、@Post
、@Put
、@Delete
が用意されていて、リクエストを受けるとリクエストメソッドに対応するアノテーションが付いた関数が呼び出されます。@Get(':id')
のようにコロンで始まる文字列を渡した場合、req.params.id
でパラメーターを受け取ることができます。http://localhost:3000/api/songs/1
にGETリクエストを送るとreq.params.id
は1になります。
import { Request, Response } from 'express';
import { Controller, Get, Post, Put, Delete } from '@overnightjs/core';
import { Logger } from '@overnightjs/logger';
import { StatusCodes } from 'http-status-codes';
interface ISong {
id: number;
title: string;
artist: string;
}
interface ISongGetRequestParams {
id: number;
}
interface ISongUpdateRequestParams {
id: number;
}
interface ISongDeleteRequestParams {
id: number;
}
@Controller('api/songs')
export class SongController {
@Get('')
private async getAll(req: Request<void, ISong[], void, void>, res: Response<ISong[]>): Promise<Response<ISong[]>> {
// ほんとはDBとかからデータ取得する
const songs: ISong[] = [
{id: 1, title: 'So Sweet', artist: '諏訪ななか'},
{id: 2, title: 'クローバー', artist: '楠木ともり'}
];
return res.status(StatusCodes.OK).json(songs);
}
@Get(':id')
private async get(req: Request<ISongGetRequestParams, ISong, void, void>, res: Response<ISong>): Promise<Response<ISong>> {
Logger.Info(req.params, true);
// ほんとはDBとかからデータ取得する
const song: ISong = {
id: 1,
title: 'So Sweet',
artist: '諏訪ななか'
};
return res.status(StatusCodes.OK).json(song);
}
@Post()
private async add(req: Request<void, void, ISong, void>, res: Response<void>): Promise<response<void>> {
Logger.Info(req.body, true);
const song: ISong = req.body;
// ほんとはDBとかにデータ追加する
return res.status(StatusCodes.OK).json();
}
@Put(':id')
private async update(req: Request<ISongUpdateRequestParams, void, ISong, void>, res: Response<void>): Promise<Response<void>> {
Logger.Info(req.body, true);
const song: ISong = req.body;
// ほんとはDBとか更新する
return res.status(StatusCodes.OK).json();
}
@Delete(':id')
private async delete(req: Request<ISongDeleteRequestParams, void, void, void>, res: Request<void>): Promise<Response<void>> {
Logger.Info(req.params, true);
const id: number = req.params.id;
// ほんとはDBとかからデータ削除する
return res.status(StatusCodes.OK).json();
}
}
サーバー
Expressに対してあれこれ設定して、コントローラーをインスタンス化してOvernightJSに登録して待ち受けを開始します。
ここではthis.app
がExpressのインスタンスなので、普通にExpressを使うときと同様にミドルウェア登録したりとかいろいろできます。OvernightJSのServer
がExpressをラップしてる感じですね。
import * as bodyParser from 'body-parser';
import cors from 'cors';
import { Server } from '@overnightjs/core';
import { Logger } from '@overnightjs/logger';
import { SongController } from './controller/SongController';
class SongServer extends Server {
constructor() {
super(process.env.NODE_ENV === 'development');
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({extended: true}));
this.app.use(cors());
this.setupControllers();
}
private setupControllers(): void {
const songController: SongController = new SongController();
super.addControllers([songController]);
}
public start(port: number): void {
this.app.listen(port, (): void => {
Logger.Imp(`server listening on port ${port}`);
});
}
}
export default SongServer;
index.ts
エントリーポイントですが、やることはServerを叩くだけです。
import SongServer from './SongServer';
const server: SongServer = new SongServer();
server.start(3000);
起動
$ npm run dev:watch
これでホットリロードありでサーバーが起動します。
おわりに
TypeScriptでExpressのアプリケーション作る場合はexpress-generator
で生成したJavaScript一式をTypeScriptに書き直すという苦行も1つの手段としてありますが、OvernightJSのようなデコレーターを使うとちゃんとTypeScriptらしい書き方で、かつシンプルにREST APIを実装できます。凝ったことしなければRouterとか意識する必要すらないです(今回は触れてないですが、複雑な要件のためにOvernightJSはMiddlewareやRouterを扱う機能も持っています)。
Express+TypeScriptの環境でAPIサーバーをさくっと作りたい方、検討してみるとよいかと思います。