0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OvernightJSでNode+Express+TypeScriptなAPIサーバーを構築する

Last updated at Posted at 2020-10-29

日本語情報が全く見当たらないExpressのTypeScript decoratorであるOvernightJSを使ってみたので、導入方法をざっくりまとめます。

Node/ExpressでTypeScriptを使う方法については、簡単に書きはしますが本筋ではないので詳細は他を当たってください。このあたりとか参考になります。

前提環境

  • Node 12.18.4
  • Express 4.17.1
  • TypeScript 4.0.3

開発環境でのTypeScriptの実行にはts-nodets-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が生成されるので、その中にあるtargetES2019に、esModuleInteropexperimentalDecoratorsemitDecoratorMetadataのコメントを外してtrueに設定します。

package.jsonのscripts部分はこんな感じにしました。ここはOvernightJS固有の要素は特になくTypeScript使う場合の一般的な設定内容です。開発環境ではts-nodeやts-node-devでTypeScriptファイルを直接実行、本番環境ではトランスパイルします。

package.json
  "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になります。

controller/SongController.ts
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をラップしてる感じですね。

SongServer.ts
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を叩くだけです。

index.ts
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サーバーをさくっと作りたい方、検討してみるとよいかと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?