Node.js
TypeScript
Fastify

Fastify×TypeScriptを実務で使うための構成を考える

FastifyをTypeScriptで利用する方法で作成したプロジェクトをベースに、実際に実務で使える構成を考えてみようと思います。

満たしたい要件は以下の通り。

  • エンドポイントごとにフォルダを分ける
  • エンドポイントの管理、コントローラ、サービスなどをファイルごとに分ける
  • 非同期処理を扱う
  • 型を定義する

エンドポイントごとにフォルダを分ける

サンプル通りに実装すると、起点となるserver.tsで全てのエンドポイントを管理することになってしまうので、これを各エンドポイントごとに管理し、それぞれでフォルダ分けできるようにします。

src/server.ts
// サンプルはこんな感じ
import * as fastify from 'fastify'
import * as cors from 'cors'

const server = fastify();

class REST {
  constructor() {
    this.init();
  }

  init() {
    server.use(cors());
    server.get('/', this.getHelloHandler);

    server.listen(3000, err => {
      if (err) throw err;
      console.log(`server listening on ${server.server.address().port}`);
    });
  }

  getHelloHandler (req: any, reply: any) {
    reply.header('Content-Type', 'application/json').code(200);
    reply.send({ hello: 'world' });
  }
}

エンドポイントごとに処理を管理するクラスを作成し、それらを纏めるファイルをserver.tsで読み込む構成にします。

src/hoge/hoge.routes.ts
// エンドポイントごとに処理を管理する
import * as fastify from 'fastify';

export class HogeRoutes {
  initRoutes(server: any, opts: any, next: any): void  {
    server.get('/hoge', this.getHogeHandler);
    server.post('/hoge', this.getHogeHandler);

    next();
  }

  getHogeHandler(req: any, reply: any) {
    reply.header('Content-Type', 'application/json').code(200);
    reply.send({ hello: 'world' });
  }

  postHogeHandler(req: any, reply: any) {
      :
  }
}
src/router.ts
// エンドポイントごとに作成されたroutesファイルを纏める
import * as fastify from 'fastify';
import { HogeRoutes } from './hoge/hoge.routes';
import { FugaRoutes } from './fuga/fuga.routes';

export function Router(server: any): void {
  const hogeRoutes = new HogeRoutes();
  const fugaRoutes = new FugaRoutes();

  server.register([
    hogeRoutes.initRoutes,
    fugaRoutes.initRoutes,
  ]);
};
src/server.ts
import * as fastify from 'fastify'
import * as cors from 'cors'
import { Router } from './router';

const server = fastify();

class REST {
  constructor() {
    this.init();
  }

  public init(): void {
    server.use(cors());

    // 作成したRouterを呼ぶ
    Router(server);

    server.listen(3000, err => {
      if (err) throw err;
      console.log(`server listening on ${server.server.address().port}`);
    });
  }
}

new REST();

エンドポイントの管理、コントローラ、サービスなどをファイルごとに分ける

先ほどはroutesファイルの中で各エンドポイントで呼ばれるファンクションを管理していましたが、それらを切り出してコントローラーとします。

src/hoge/hoge.routes.ts
import * as fastify from 'fastify';
import { HogeController } from './hoge.controller';

export class HogeRoutes {
  initRoutes(server: any, opts: any, next: any): void  {
    const hogeController = new HogeController();

    server.get('/hoge', hogeController.getHogeHandler);
    server.post('/hoge', hogeController.postHogeHandler);

    next();
  }
}
src/hoge/hoge.controller.ts
import * as fastify from 'fastify';
import { HogeService } from './hoge.service';

export class HogeController {
  constructor(){ }

  public getHogeHandler(req: any, reply: any) {
    let hogeService = new HogeService();
    let res = hogeService.getHoge();

    reply.header('Content-Type', 'application/json').code(200);
    reply.send({
      'status': '00000',
      'message': '',
      'data': res,
    });
  }


  public postSoilMoistureHandler(req: any, reply: any) {
      :
  }
}

非同期処理を扱う

HogeServiceの処理が非同期処理だった場合には、async/awaitを使います。

src/hoge/hoge.controller.ts
import * as fastify from 'fastify';
import { HogeService } from './hoge.service';

export class HogeController {
  constructor(){ }

  public async getHogeHandler(req: any, reply: any) {
    let hogeService = new HogeService();
    let res = await hogeService.getHoge();

    reply.header('Content-Type', 'application/json').code(200);
    return {
      'status': '00000',
      'message': '',
      'data': res,
    };
  }


  public async postHogeHandler(req: any, reply: any) {
      :
  }
}

型を定義する

各引数や戻り値などの型を定義します。
例えば、hoge.controller.tsの場合だと以下のようになります。

src/hoge/hoge.controller.ts
import * as fastify from 'fastify';
import { HogeService } from './hoge.service';
import { IResponse } from '../common/interfaces/response.interface';

export class HogeController {
  constructor(){ }

  public async getHogeHandler(req: fastify.FastifyRequest, reply: fastify.FastifyReply): Promise<IResponse> {
    let hogeService = new HogeService();
    let res = await hogeService.getHoge();

    reply.header('Content-Type', 'application/json').code(200);
    return {
      'status': '00000',
      'message': '',
      'data': res,
    };
  }


  public async postHogeHandler(req: fastify.FastifyRequest, reply: fastify.FastifyReply): Promise<IResponse> {
      :
  }
}
src/common/interfaces/response.interface.ts
export interface IResponse {
  status: string;
  message: string;
  data?: any[] | object;
}

以上です。
これでだいぶ管理しやすくなったと思います。
ディレクトリは以下のような構成になります。

.
├── README.md
├── gulpfile.js
├── package-lock.json
├── package.json
├── src
│   ├── common
│   │   └── interfaces
│   │       └── response.interface.ts
│   ├── fuga
│   │   ├── fuga.controller.ts
│   │   ├── fuga.routes.ts
│   │   └── fuga.service.ts
│   ├── hoge
│   │   ├── hoge.controller.ts
│   │   ├── hoge.routes.ts
│   │   └── hoge.service.ts
│   ├── router.ts
│   └── server.ts
└── tsconfig.json