【tsoa × Express】OpenAPI仕様書を自動生成する手順

Backend
Backend

tsoa x Express を使ってAPI仕様書の自動生成環境をセットアップした際の、学習の備忘録です。

  • tsoaの概要と仕組み
  • Express環境での導入手順と設定方法

tsoaの理解

tsoaとは OpenAPI仕様書(Swagger)を自動生成するためのライブラリ です。
Express や NestJS などの既存のサーバーに組み込むことができます。

tsoaを使うと、通常のExpressの書き方とは異なり、
tsoaが提供するデコレータ(@)構文を使ってControllerやルートを定義します。

ここからは、tsoaを導入してAPI仕様書を自動生成できるようにするまでの手順を解説します。

1. パッケージインストール

npm i tsoa swagger-ui-express
npm i -D concurrently @types/swagger-ui-express

2. tsoa.json の設定

API仕様書の自動生成を行うための初期設定です。

{
    "entryFile": "src/app.ts",

    // 追加のプロパティを許可しない設定。予期しないプロパティがあればthrowする
    "noImplicitAdditionalProperties": "throw-on-extras",
    
    // どのファイルがコントローラーとして扱われるか指定
    "controllerPathGlobs": ["src/**/*Controller.ts"],
    
    // API仕様書の設定
    "spec": {
        // 生成された仕様書が保存されるディレクトリ
        "outputDirectory": "build",
        
        // 仕様のバージョン
        "specVersion": 3
    },
    
    // ルーティングに関する設定
    "routes": {

        // ルーティングの設定ファイルが生成されるディレクトリ
        "routesDir": "build",
        
        // ミドルウェア
        "middleware": {
            "auth": "./src/middleware/authMiddleware"
        }
    }
}
  • "middleware":定義のみ(実際の適用はコントローラ側で指定)

3. tsconfig.json の設定

experimentalDecoratorsemitDecoratorMetadata を true にしました。他は任意の設定でOKです。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./build",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

4. 認証ミドルウェア(authMiddleware.ts)

作成は任意ですが、認証を行う場合の例として記載します。

// src/middleware/authMiddleware.ts

import { Request, NextFunction, Response } from "express";

export interface AuthenticatedRequest extends Request {
    userId?: string;
    req: AuthenticatedRequest;
}

export const authMiddleware = (
    req: AuthenticatedRequest,
    res: Response,
    next: NextFunction
) => {
    // リクエストヘッダーからトークンを取得
    const token = req.headers["x-access-token"];

    // トークンが存在しない場合は401エラーを返す
    if (!token) {
        return res.status(401).json({ message: "No token privided" });
    }

    // 仮のトークン値を設定
    const expectedToken = "your-hardcoded-token-value";

    // トークンのチェック
    if (token === expectedToken) {
        // トークンが正しい場合、仮のユーザーIDをリクエストオブジェクトに追加
        req.userId = "123"; // 仮のユーザーID
        next(); // 認証成功。次のミドルウェアへ進む
    } else {
        return res.status(403).json({ message: "Invalid token" });
    }
};

5. 型定義ファイル(user.ts)

後述の userController.ts で使用する型定義です。

// src/users/user.ts
export interface User {
  id: number;
  email: string;
  name: string;
  status?: "Happy" | "Sad";
  phoneNumbers: string[];
}

6. コントローラーファイル(userController.ts)

tsoaの肝です。
ここに定義した内容をもとに、API仕様書が自動生成されます。

// src/users/userController.ts

import {
    Body,
    Controller,
    Get,
    Header,
    Middlewares,
    Path,
    Post,
    Query,
    Request,
    Response,
    Route,
    SuccessResponse,
} from "tsoa";
import { User } from "./user";
import { UserCreationParams, UsersService } from "./usersService";
import {
    AuthenticatedRequest,
    authMiddleware,
} from "../middleware/authMiddleware";

interface ValidateErrorJSON {
    message: "Validation failed";
    details: { [name: string]: unknown };
}

// このファイルで定義するエンドポイントを/usersに定義
@Route("users")
@Middlewares([authMiddleware])
export class UsersController extends Controller {
    @Get("{userId}")
    public async getUser(
        @Header("X-Access-Token") _token: string,
        @Request() req: AuthenticatedRequest,
        @Path() userId: number,
        @Query() name?: string
    ): Promise<User> {
        return new UsersService().get(userId, name);
    }

    @Response<ValidateErrorJSON>(422, "Validation Failed")
    @SuccessResponse("201", "Created")
    @Post()
    public async createUser(
        @Header("X-Access-Token") _token: string,
        @Body() requestBody: UserCreationParams
    ): Promise<void> {
        this.setStatus(201);
        new UsersService().create(requestBody);
        return;
    }
}

7. SwaggerUI ミドルウェア設定

// src/app.ts

import express, {
    json,
    urlencoded,
    Response as ExResponse,  // ExはExpress用の型定義だとわかりやすくするため
    Request as ExRequest,
    NextFunction,
} from "express";
import swaggerUi from "swagger-ui-express";
import { RegisterRoutes } from "../build/routes";
import { ValidateError } from "tsoa";

export const app = express();

app.use(urlencoded({ extended: true }));
app.use(json());

// SwaggerUIを生成するミドルウェア
// http://localhost:3000/docsで参照
app.use("/docs", swaggerUi.serve, async (_req: ExRequest, res: ExResponse) => {
    res.send(swaggerUi.generateHTML(await import("../build/swagger.json")));
});

app.use(function errorHandler(
    err: unknown,
    req: ExRequest,
    res: ExResponse,
    next: NextFunction
) {
    // ValidateError:tsoa専用のエラークラス。リクエストパラメータ/ボディのバリデーション失敗時に発生
    if (err instanceof ValidateError) {

        // fields:リクエストパラメータ/ボディのキーとバリデーションエラーの詳細
        // 例: { fields: { "age": { message: "Age must be a positive number" } } }
        console.warn(`Caught Validation Error for ${req.path}:`, err.fields);
        
        // 422ステータスとmessage, detailsを返す
        res.status(422).json({
            message: "Validation Failed",
            details: err?.fields,
        });
    }
    if (err instanceof Error) {
        // 500ステータスとmessageを返す
        res.status(500).json({
            message: "Internal Server Error",
        });
    }
    next();
});

// tsoaが自動生成したルーティング情報を Express アプリケーションに登録
RegisterRoutes(app);
  • swaggerUi.serve:SwaggerUIを実行するミドルウェア
  • _req:未使用の引数にアンダースコア。未使用だけどreqは記述しなければならないため
  • generateHTML:Swagger.jsonを読み込んでHTML化

8. package.json のスクリプト設定

package.jsonからscriptsのみ抜粋して解説します。

"scripts": {
    // コード変更の度にAPI仕様書を自動で最新化
    // 左側の nodemonはアプリケーションサーバーを再起動
    // 右側の nodemonは APIのスペックとルートを自動再生成
    "dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec-and-routes\"",
    
    // コンパイルしてAPI仕様書を生成し、TypeScriptをJavaScriptに変換
    "build": "tsoa spec-and-routes && tsc",
    "start": "node build/src/server.js"
  },
  • concurrently: 複数のコマンドを同時に実行するためのツール
  • nodemon: コードの変更を監視し、自動的にサーバーを再起動するツール。-xで特定のコマンドを実行する。
  • tsoa spec-and-routes: APIの仕様とルーティングを生成
  • tsc: TypeScriptコードをJavaScriptにコンパイル

9. API仕様書の生成コマンド

開発中:

npm run dev
# http://localhost:3000/docs にアクセス

デプロイ時:

npm run build
# buildディレクトリとswagger.jsonが自動生成されます

まとめ

以上で、tsoaを使ったOpenAPI仕様書の自動生成までの一連の手順が完了しました。
tsoaを導入することで、API設計と実装の一貫性を保ちながら、ドキュメントも自動化できます。

開発チームでAPI仕様を共有する際にも非常に便利なので、ぜひ活用してみてください。

コメント

タイトルとURLをコピーしました