はじめに
TypescriptでWebAPIのExpressサーバを実装した時のまとめです。
なるべくシンプルかつ拡張性の高い構成になるように整理しました。
前提
Typescriptのビルド環境は構築済みとします。
インストール
npm install --save express body-parser superagent
npm install --save-dev @types/express @types/body-parser @types/superagent
ソースコード
クライアントからリクエスト受けると、サーバは別プロセスのとある解析エンジンを実行し、結果をクライアントに返すというプログラムの例です。
サーバ側
エントリーポイント
import ExpressServer from "./web/ExpressServer";
ExpressServer.start();
import * as Express from "express";
import * as bodyParser from "body-parser";
import analysis from "./routes/AnalysisRouter";
export default class ExpressServer {
public static start(): void {
const app: Express.Application = Express();
app.listen(3000, () => {
console.log("app listening on port 3000!");
});
app.use((req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
//CORSの設定。別ドメインからのアクセスを許可する。
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});
//JSONをパースしてくれる
app.use(bodyParser.json());
//機能が増えるとここに足されていく
app.use("/analysis", analysis);
}
}
ExpressServer.tsにはExpressサーバ共通のことのみ実装します。
ここでは、サーバの起動、HTTPヘッダの設定、bodyParserの設定、各Routerの設定を行っています。
import * as Express from "express";
import AnalysisRequestData from "../../../../lib/data/api/AnalysisRequestData";
import EngineController from "../../engine/EngineController";
import AnalysisResponseData from "../../../../lib/data/api/AnalysisResponseData";
import EngineResponseData from "../../../../lib/data/EngineResponseData";
class AnalysisRouter {
public static create(): Express.Router {
const router = Express.Router();
//エンドポイントの数だけ増える
router.post("/", this.root.bind(this));
return router;
}
private static root(req: Express.Request, res: Express.Response): void {
const reqData: AnalysisRequestData = AnalysisRequestData.fromJSON(req.body);
(async () => {
const resData = await this.execEngine(reqData);
res.json(resData);
})();
}
private static async execEngine(reqData: AnalysisRequestData): Promise<AnalysisResponseData> {
const data: EngineResponseData = await EngineController.instance()
.exec(reqData.piecePosition, reqData.engineCommand, reqData.engineOption);
return new AnalysisResponseData(reqData.apiName, data);
}
}
export default AnalysisRouter.create();
各機能ごとにURIを分け、それぞれクラスを作ります。
ここではAnalysisRouterというクラスを作って、各エンドポイントごとにメソッドを分けています。
MVCでいうところのControllerに位置づけされるクラスですね。なので、AnalysisControllerというクラス名でもいいかと思います。
AnalysisRequestData、AnalysisResponseDataはデータクラスです。
クライアントからAnalysisRequestDataを受信し、AnalysisResponseDataを返すという作りになっています。
一部抜粋
export default class AnalysisRequestData {
...
constructor(
private _apiName: ApiName,
private _piecePosition: PiecePosition,
private _engineCommand: EngineCommand,
private _engineOption: EngineOption,
){}
...
public static fromJSON(obj: Json): AnalysisResponseData {
...
}
データクラスはこんな感じです。AnalysisResponseDataも同様の作りです。
##クライアント側
import * as request from "superagent";
import AnalysisResponseData from "../../../../lib/data/api/AnalysisResponseData";
import AnalysisRequestData from "../../../../lib/data/api/AnalysisRequestData";
export default class AnalysisClient {
public analyze(requestData: AnalysisRequestData): Promise<AnalysisResponseData> {
return new Promise<AnalysisResponseData>(
(resolve, reject) => {
request.post("http://localhost:3000/analysis")
.send(requestData)
.then((res) => {
resolve(AnalysisResponseData.fromJSON(res.body));
});
}
);
}
}
クラインアントはシンプルに実装できるsuperagentを使っています。
AnalysisRequestDataとAnalysisResponseDataはサーバ側と同じコードを使用しています。
サーバとクライアントの言語が一緒だと、インターフェースを共通で使えます。
何の変換もなしにsend(requestData)としていますが、問題ありません。
内部的にはJSON.stringify(requestData)の結果が送信されています。
AnalysisRequestDataの実態はObject型なので、JSON.stringifyすると{"_apiName": ..., "_piecePosition": ...}というように、インスタンス変数がJSON化されます。
const client: AnalysisClient = new AnalysisClient();
(async () => {
const data: AnalysisResponseData = await client.analyze(requestData);
...
})();
非同期処理をPromiseで実装しているので、呼び出し側はこのようなシンプルな実装で使えます。
#まとめ
拡張性が高くなるように整理しました。
別の機能を追加する時は、サーバ側はxxxRouter.tsを、クライアント側はxxxClient.tsを増やしていけばいいので、それぞれ機能ごとに独立して実装できるかと思います。
また、インターフェースのコードを共通化できるのが素晴らしいです。
APIの型定義ができて、コードを共通化できるというのは、ブラウザでもサーバでも動くTypeScriptならではのメリットだと思います。
※ちなみに、今回の実装ではエラー処理は何も考慮していません。