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-express2. 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 の設定
experimentalDecorators と emitDecoratorMetadata を 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仕様を共有する際にも非常に便利なので、ぜひ活用してみてください。
  
  
  
  
コメント