TypeScript で Spectral を使う方法

Audrey Lopez

Audrey Lopez

19 5月 2025

TypeScript で Spectral を使う方法

APIを使用するチームにとって、一貫性、品質、および設計標準への準拠を維持することは最も重要です。OpenAPIやAsyncAPIのようなAPI仕様は設計図を提供しますが、これらの設計図が多数のサービスやチーム全体で正しく遵守されていることを保証するのは、困難な課題となり得ます。ここでAPIリンティングツールが活躍し、Spectralは柔軟で強力なオープンソースの選択肢として際立っています。TypeScriptと組み合わせることで、Spectralは開発者が堅牢でタイプセーフなカスタムルールを作成することを可能にし、APIガバナンスを新たなレベルに引き上げます。

このチュートリアルでは、SpectralをTypeScriptと連携させるプロセスを、初期セットアップから洗練されたカスタム検証ロジックの作成までガイドします。TypeScriptがSpectralルールの開発をどのように強化し、より保守しやすく信頼性の高いAPIリンティングソリューションにつながるかを探求します。

💡
美しいAPIドキュメントを生成する素晴らしいAPIテストツールをお探しですか?

最大限の生産性で開発チームが共同作業できる統合されたオールインワンプラットフォームをお探しですか?

Apidogはあなたの全ての要求を満たし、Postmanをはるかに手頃な価格で置き換えます
button

Spectralを理解する:API仕様の守護者

TypeScriptとの連携に入る前に、まずSpectralとは何か、そしてなぜそれがAPI開発ツールキットにおいて価値のあるツールなのかを確立しましょう。

Spectralは、OpenAPI (v2およびv3)AsyncAPIのようなAPI記述フォーマットに焦点を当てたオープンソースのJSON/YAMLリンターです。その目的は、API設計ガイドラインの強制、一般的なエラーの検出、APIランドスケープ全体の一貫性の確保を支援することです。APIコントラクトのためのESLintやTSLintのようなものだと考えてください。

Spectralを使用する主な利点:

Spectralはルールセットに基づいて動作します。ルールセットはルールの集合であり、各ルールはAPIドキュメントの特定の箇所(JSONPath式を使用)をターゲットにし、検証ロジックを適用します。Spectralには組み込みのルールセット(例:OpenAPI標準のためのspectral:oas)が付属していますが、その真の力はカスタムルールセットを定義できることにあります。

SpectralカスタムルールにTypeScriptを使用する理由

SpectralルールセットはYAMLまたはJavaScript(.jsファイル)で定義できますが、カスタム関数の開発にTypeScriptを使用すると、大きな利点があります。

カスタムSpectral関数をTypeScriptで記述することで、アプリケーションコードと同じ厳格さとツールによる利点をAPIガバナンスコードにもたらすことができます。

SpectralとTypeScript環境のセットアップ

実践に移り、必要なツールをセットアップしましょう。

前提条件:

インストール手順:

まず、リンティング操作を実行し、ルールをテストするためにSpectral CLIが必要です。通常、グローバルにインストールするか、npxを使用するのが便利です。Bash

npm install -g @stoplight/spectral-cli
# or
yarn global add @stoplight/spectral-cli

TypeScriptプロジェクト内でプログラム的にカスタムルールを開発し、Spectralのコアライブラリを使用するには、必要なSpectralパッケージをインストールします。Bash

npm install @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --save-dev
# or
yarn add @stoplight/spectral-core @stoplight/spectral-functions @stoplight/spectral-rulesets typescript ts-node --dev

これらのパッケージを分解してみましょう。

TypeScriptの設定:

まだtsconfig.jsonファイルがない場合は、プロジェクトルートに作成してください。基本的な設定は次のようになります。JSON

{
  "compilerOptions": {
    "target": "es2020", // またはより新しいバージョン
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist", // コンパイルされたJavaScriptの出力ディレクトリ
    "rootDir": "./src", // TypeScriptファイルのソースディレクトリ
    "resolveJsonModule": true // JSONファイルのインポートを許可
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

プロジェクト構造に合わせてoutDirrootDirを調整してください。カスタムTypeScript関数はsrcディレクトリに配置すると仮定します。

Spectralのコアコンセプト:ルール、ルールセット、関数

TypeScript関数を記述する前に、Spectralの主要なコンポーネントの理解を固めましょう。

ルール:

ルールは実行される特定のチェックを定義します。ルールの主なプロパティは次のとおりです。

ルールセット:

ルールセットは、ルールをグループ化するYAMLまたはJavaScriptファイル(例:.spectral.yaml、.spectral.js、またはコンパイルされた.spectral.ts)です。また、次のことも可能です。

関数:

関数は、実際の検証を実行するコアロジック単位です。Spectralは、次のような多くの組み込み関数を提供します。

これらの組み込み関数だけでは不十分な場合に、独自のカスタム関数を記述する必要があり、ここでTypeScriptが威力を発揮します。

TypeScriptで最初のカスタムSpectral関数を作成する

簡単なカスタム関数を作成してみましょう。すべてのAPI操作サマリーがタイトルケースであり、70文字を超えないというルールを強制したいと想像してください。

ステップ1:TypeScriptでカスタム関数を定義する

たとえば、src/customFunctions.tsというファイルを作成します。TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

interface TitleCaseLengthOptions {
  maxLength: number;
}

// 文字列がタイトルケースであり、最大長以内であるかチェックするカスタム関数
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // 'given' パスが文字列を指している場合は発生しないはずですが、良い習慣です
    return [{ message: `パス '${context.path.join('.')}' の値は文字列である必要があります。` }];
  }

  // タイトルケースのチェック(簡単なチェック:各単語の最初の文字が大文字であること)
  const words = targetVal.split(' ');
  const isTitleCase = words.every(word => word.length === 0 || (word[0] === word[0].toUpperCase() && (word.length === 1 || word.substring(1) === word.substring(1).toLowerCase())));

  if (!isTitleCase) {
    results.push({
      message: `パス '${context.path.join('.')}' のサマリー "${targetVal}" はタイトルケースである必要があります。`,
      path: [...context.path], // 違反している要素へのパス
    });
  }

  // 長さのチェック
  const maxLength = options?.maxLength || 70; // 指定がない場合は70をデフォルトとする
  if (targetVal.length > maxLength) {
    results.push({
      message: `パス '${context.path.join('.')}' のサマリー "${targetVal}" は最大長 ${maxLength} 文字を超えています。現在の長さ:${targetVal.length}。`,
      path: [...context.path],
    });
  }

  return results;
};

説明:

ステップ2:TypeScript関数をコンパイルする

この関数をfunctionsDirを指す.spectral.yamlまたは.spectral.jsルールセットで使用する予定がある場合は、TypeScriptをJavaScriptにコンパイルする必要があります。

package.jsonにビルドスクリプトを追加します。JSON

{
  "scripts": {
    "build": "tsc"
  }
}

npm run buildまたはyarn buildを実行します。これにより、src/customFunctions.tsdist/customFunctions.jsにコンパイルされます(tsconfig.jsonに基づきます)。

ステップ3:ルールセットファイルを作成する

たとえば、.spectral.jsというルールセットを作成しましょう(完全にTypeScript主導のセットアップを好む場合は.spectral.ts、次のセクションを参照)。

コンパイルされた関数を直接参照するJavaScriptルールセットファイルを使用する場合:JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // コンパイルされたJS関数へのパス

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: '操作サマリーはタイトルケースであり、70文字を超えてはなりません。',
      message: '{{error}}', // メッセージはカスタム関数から来ます
      given: '$.paths[*][*].summary', // すべての操作サマリーをターゲットにします
      severity: 'warn',
      formats: ["oas3"], // OpenAPI v3ドキュメントにのみ適用
      then: {
        function: titleCaseAndLength, // インポートされた関数を直接参照
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // ここにさらにルールを追加できます
  },
};

または、.spectral.yamlfunctionsDirを使用する場合:

まず、distディレクトリに、関数をエクスポートするindex.jsが含まれているか、またはcustomFunctions.jsが直接エクスポートしていることを確認します。たとえば、dist/customFunctions.jsexports.titleCaseAndLength = ...;がある場合、次のようにできます。YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # コンパイルされたカスタムJS関数を含むディレクトリ
rules:
  operation-summary-title-case-length:
    description: "操作サマリーはタイトルケースであり、70文字を超えてはなりません。"
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # customFunctions.jsから関数がエクスポートされていると仮定
      functionOptions:
        maxLength: 70

ここで、customFunctions#titleCaseAndLengthは、SpectralにfunctionsDir内のcustomFunctions.js(またはcustomFunctions/index.js)を探し、エクスポートされたtitleCaseAndLength関数を使用するように指示します。

ステップ4:サンプルOpenAPIドキュメントを作成する

ルールをテストするための簡単なopenapi.yamlファイルを作成しましょう。YAML

# openapi.yaml
openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: retrieves all items from the store # 不正:タイトルケースではない
      responses:
        '200':
          description: アイテムのリスト。
    post:
      summary: Adds A New Item To The Ever Expanding Collection Of Items In The Store # 不正:長すぎる
      responses:
        '201':
          description: アイテムが作成されました。
  /users:
    get:
      summary: Get User Details # 正しい
      responses:
        '200':
          description: ユーザー詳細。

ステップ5:Spectralを実行する

次に、OpenAPIドキュメントに対してSpectral CLIを実行します。Bash

spectral lint openapi.yaml --ruleset .spectral.js
# またはYAMLルールセットを使用する場合(.spectral.yamlという名前の場合は自動検出されることが多い)
spectral lint openapi.yaml

期待される出力:

次のような警告が表示されるはずです。

openapi.yaml
 2:10  warning  operation-summary-title-case-length  パス 'paths./items.get.summary' のサマリー "retrieves all items from the store" はタイトルケースである必要があります。   paths./items.get.summary
 6:10  warning  operation-summary-title-case-length  パス 'paths./items.post.summary' のサマリー "Adds A New Item To The Ever Expanding Collection Of Items In The Store" は最大長 70 文字を超えています。現在の長さ:78。  paths./items.post.summary

✖ 2 problems (0 errors, 2 warnings, 0 infos, 0 hints)

この出力は、コンパイルされたJavaScriptであるカスタムTypeScript関数が、違反を正しく識別していることを確認しています。

TypeScriptルールセットによるSpectralのプログラム的使用

より複雑なシナリオやアプリケーションへのより密接な統合のために、Spectralをプログラム的に使用し、ルールセット全体をTypeScriptで定義したい場合があります。これにより、必要に応じて個別の.spectral.yamlまたは.spectral.jsファイルが不要になり、動的なルールセット構築が可能になります。

たとえば、src/linter.tsというリンター用のTypeScriptファイルを作成します。TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // 組み込み関数の例
import { titleCaseAndLength } from './customFunctions'; // あなたのカスタムTS関数
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// Spectralインスタンスを定義
const spectral = new Spectral();

// 組み込みルールセットと関数をロード
spectral.setRuleset({
  extends: [[oas, 'recommended']], // 推奨されるOpenAPIルールを継承
  rules: {
    'operation-summary-title-case-length': {
      description: '操作サマリーはタイトルケースであり、70文字を超えてはなりません。',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // TypeScript関数を直接使用
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // 組み込み関数を使用した例
        description: 'Infoオブジェクトはcontactオブジェクトを持つべきです。',
        message: 'API連絡先情報がありません。',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // 組み込み関数を使用
        },
      },
  },
});

// ドキュメントをリントする関数
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // Spectral Documentオブジェクトを作成
    // 2番目の引数(URI)は、$refがあれば相対パスを解決するために重要です
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('ドキュメントのリントエラー:', error);
    return [];
  }
}

// 使用例(例:スクリプトまたは別のモジュールで)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // API仕様へのパス
  if (diagnostics.length > 0) {
    console.log('APIリンティングの問題が見つかりました:');
    diagnostics.forEach(issue => {
      console.log(
        `- [${issue.severity === 0 ? 'Error' : issue.severity === 1 ? 'Warning' : issue.severity === 2 ? 'Info' : 'Hint'}] ${issue.code} (${issue.message}) at ${issue.path.join('.')}`
      );
    });
  } else {
    console.log('APIリンティングの問題は見つかりませんでした。素晴らしい!');
  }
}

// このファイルをts-nodeで直接実行したい場合
if (require.main === module) {
  main().catch(console.error);
}

これを実行するには:Bash

npx ts-node src/linter.ts

このアプローチは最大の柔軟性を提供します。

高度なカスタム関数テクニック

カスタム関数の作成に関するより高度な側面を探求しましょう。

非同期カスタム関数:

カスタム関数が非同期操作(例:外部リソースをフェッチして検証する、ただしパフォーマンスに注意して使用)を実行する必要がある場合、それをasync関数として定義できます。TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';

export const checkExternalResource: IFunction<string, { url: string }> = 
  async (targetVal, options, context): Promise<IFunctionResult[] | void> => {
  try {
    const response = await fetch(`${options.url}/${targetVal}`);
    if (!response.ok) {
      return [{ message: `リソース '${targetVal}' は ${options.url} で見つかりませんでした。ステータス:${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `リソース '${targetVal}' のフェッチエラー: ${error.message}` }];
  }
};

Spectralは非同期関数を正しくawaitします。

ドキュメント全体と解決済み値へのアクセス:

context.documentオブジェクトを使用すると、解析されたドキュメント全体にアクセスできます。さらに強力なのは、context.document.resolvedです。これはドキュメントの完全に逆参照されたバージョンを提供し、$refポインターを扱う際に不可欠です。TypeScript

import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
import {isPlainObject} from '@stoplight/json';

// 例:参照されているスキーマが特定のプロパティを持っていることを確認する
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // これは { $ref: '#/components/schemas/MySchema' } のようなオブジェクトになります
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // `context.document.resolveAnchor` メソッドは、$refの解決済み値を見つけることができます
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `$ref を解決できませんでした: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `パス '${context.path.join('.')}' で "${targetVal.$ref}" によって参照されるスキーマは、プロパティ "${options.propertyName}" を持っている必要があります。`,
      path: [...context.path, '$ref'] // $ref自体を指す
    }];
  }
};

この関数は、$refを含むオブジェクトをターゲットにするgivenパス(例:$.paths[*].*.responses.*.content.*.schema)とともに使用されます。

フォーマットの扱い:

カスタムルールが適用されるフォーマット(例:oas2、oas3、asyncapi2)を指定していることを確認してください。これにより、互換性のないドキュメントタイプでルールが実行されるのを防ぎます。関数内では、context.document.formatsを介して検出されたフォーマットにアクセスできます。

ルールセットの整理と拡張

カスタムルールのコレクションが増えるにつれて、整理が鍵となります。

// .spectral.js (メインルールセット)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... その他のルールまたはオーバーライド
};

Spectralをワークフローに統合する

APIリンティングの真の力は、それが自動化されたときに実現されます。

# GitHub Actionsステップの例
- name: API仕様をリント
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

SpectralとTypeScriptカスタムルールのベストプラクティス

結論:SpectralとTypeScriptでAPIガバナンスを向上させる

SpectralはAPIリンティングのための堅牢なフレームワークを提供し、カスタムルール開発にTypeScriptを活用することで、APIガバナンス戦略に強化された型安全性、開発者エクスペリエンス、および保守性をもたらします。OpenAPIのベストプラクティス、会社固有の命名規則、またはAPI設計内の複雑なビジネスロジックを強制する場合でも、Spectralの柔軟なルールエンジンとTypeScriptの強力な型付けシステムの組み合わせは、強力なソリューションを提供します。

IDEフィードバックからCI/CDパイプラインまで、Spectralを開発ライフサイクルに統合することで、APIが一貫性があり、高品質であり、定義された標準に準拠していることを保証でき、最終的により信頼性が高く、消費しやすいAPIにつながります。小さく始め、ルールセットを繰り返し改善し、より良いAPIを構築するためのツールをチームに提供しましょう。

ApidogでAPIデザイン中心のアプローチを取る

APIの開発と利用をよりシンプルなことにする方法を発見できる