Cách sử dụng Spectral với TypeScript

Audrey Lopez

Audrey Lopez

19 tháng 5 2025

Cách sử dụng Spectral với TypeScript

Việc duy trì tính nhất quán, chất lượng và tuân thủ các tiêu chuẩn thiết kế là vô cùng quan trọng đối với các nhóm sử dụng API. Các đặc tả API như OpenAPI và AsyncAPI cung cấp một bản thiết kế, nhưng đảm bảo rằng các bản thiết kế này được tuân thủ đúng cách trên nhiều dịch vụ và nhóm có thể là một thách thức lớn. Đây là lúc các công cụ lint API phát huy tác dụng, và Spectral nổi bật như một lựa chọn mã nguồn mở linh hoạt và mạnh mẽ. Khi kết hợp với TypeScript, Spectral trao quyền cho các nhà phát triển tạo ra các quy tắc tùy chỉnh mạnh mẽ, an toàn kiểu dữ liệu, nâng quản trị API lên một tầm cao mới.

Hướng dẫn này sẽ đưa bạn qua quá trình tận dụng Spectral với TypeScript, từ thiết lập ban đầu đến việc tạo ra logic xác thực tùy chỉnh phức tạp. Chúng ta sẽ khám phá cách TypeScript nâng cao việc phát triển các quy tắc của Spectral, dẫn đến các giải pháp lint API dễ bảo trì và đáng tin cậy hơn.

💡
Bạn muốn một công cụ Kiểm thử API tuyệt vời có thể tạo Tài liệu API đẹp mắt?

Bạn muốn một nền tảng tích hợp, Tất cả trong một cho Đội ngũ Nhà phát triển của bạn làm việc cùng nhau với năng suất tối đa?

Apidog đáp ứng mọi yêu cầu của bạn, và thay thế Postman với mức giá phải chăng hơn nhiều!
Tải ứng dụng

Hiểu về Spectral: Người bảo vệ đặc tả API của bạn

Trước khi đi sâu vào tích hợp TypeScript, hãy cùng tìm hiểu Spectral là gì và tại sao nó là một công cụ có giá trị trong bộ công cụ phát triển API của bạn.

Spectral là một công cụ lint JSON/YAML mã nguồn mở tập trung chủ yếu vào các định dạng mô tả API như OpenAPI (v2 và v3)AsyncAPI. Mục đích của nó là giúp thực thi các hướng dẫn thiết kế API, phát hiện các lỗi phổ biến và đảm bảo tính nhất quán trên toàn bộ bức tranh tổng thể về API của bạn. Hãy coi nó như ESLint hoặc TSLint, nhưng dành riêng cho các hợp đồng API của bạn.

Lợi ích chính khi sử dụng Spectral:

Spectral hoạt động dựa trên bộ quy tắc (rulesets). Một bộ quy tắc là một tập hợp các quy tắc, trong đó mỗi quy tắc nhắm mục tiêu vào các phần cụ thể của tài liệu API của bạn (sử dụng biểu thức JSONPath) và áp dụng logic xác thực. Spectral đi kèm với các bộ quy tắc tích hợp sẵn (ví dụ: spectral:oas cho các tiêu chuẩn OpenAPI), nhưng sức mạnh thực sự của nó nằm ở khả năng định nghĩa các bộ quy tắc tùy chỉnh.

Tại sao lại dùng TypeScript cho các quy tắc tùy chỉnh của Spectral?

Trong khi các bộ quy tắc của Spectral có thể được định nghĩa bằng YAML hoặc JavaScript (tệp .js), việc sử dụng TypeScript để phát triển các hàm tùy chỉnh mang lại những lợi thế đáng kể:

Bằng cách viết các hàm Spectral tùy chỉnh của bạn bằng TypeScript, bạn mang lại sự chặt chẽ và lợi ích công cụ tương tự cho mã quản trị API của bạn như đối với mã ứng dụng của bạn.

Thiết lập môi trường Spectral và TypeScript của bạn

Hãy bắt tay vào làm và thiết lập các công cụ cần thiết.

Điều kiện tiên quyết:

Các bước cài đặt:

Đầu tiên, bạn sẽ cần Spectral CLI để chạy các thao tác lint và kiểm thử quy tắc của bạn. Thường hữu ích khi cài đặt nó toàn cục hoặc sử dụng npx.Bash

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

Để phát triển các quy tắc tùy chỉnh theo lập trình và sử dụng các thư viện cốt lõi của Spectral trong một dự án TypeScript, hãy cài đặt các gói Spectral cần thiết: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

Hãy giải thích các gói này:

Cấu hình TypeScript:

Tạo một tệp tsconfig.json trong thư mục gốc dự án của bạn nếu bạn chưa có. Một cấu hình cơ bản có thể trông như thế này:JSON

{
  "compilerOptions": {
    "target": "es2020", // Or a newer version
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist", // Output directory for compiled JavaScript
    "rootDir": "./src", // Source directory for your TypeScript files
    "resolveJsonModule": true // Allows importing JSON files
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Điều chỉnh outDirrootDir theo cấu trúc dự án của bạn. Chúng ta sẽ giả định các hàm TypeScript tùy chỉnh của bạn sẽ nằm trong thư mục src.

Các khái niệm cốt lõi của Spectral: Quy tắc, Bộ quy tắc và Hàm

Trước khi viết các hàm TypeScript, hãy củng cố hiểu biết của chúng ta về các thành phần chính của Spectral.

Quy tắc:

Một quy tắc định nghĩa một kiểm tra cụ thể sẽ được thực hiện. Các thuộc tính chính của một quy tắc bao gồm:

Bộ quy tắc:

Một bộ quy tắc là một tệp YAML hoặc JavaScript (ví dụ: .spectral.yaml, .spectral.js, hoặc .spectral.ts khi biên dịch) nhóm các quy tắc. Nó cũng có thể:

Hàm:

Hàm là các đơn vị logic cốt lõi thực hiện xác thực thực tế. Spectral cung cấp nhiều hàm tích hợp sẵn như:

Sức mạnh thực sự đến khi những hàm này không đủ, và bạn cần viết các hàm tùy chỉnh của riêng mình – đây là lúc TypeScript tỏa sáng.

Tạo hàm Spectral tùy chỉnh đầu tiên của bạn bằng TypeScript

Hãy tạo một hàm tùy chỉnh đơn giản. Hãy tưởng tượng chúng ta muốn thực thi một quy tắc rằng tất cả các tóm tắt hoạt động API phải được viết hoa tiêu đề (title-cased) và không vượt quá 70 ký tự.

Bước 1: Định nghĩa hàm tùy chỉnh bằng TypeScript

Tạo một tệp, ví dụ src/customFunctions.ts:TypeScript

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

interface TitleCaseLengthOptions {
  maxLength: number;
}

// Custom function to check if a string is title-cased and within a max length
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // Should not happen if 'given' path points to a string, but good practice
    return [{ message: `Value at path '${context.path.join('.')}' must be a string.` }];
  }

  // Check for Title Case (simple check: first letter of each word is uppercase)
  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: `Summary "${targetVal}" at path '${context.path.join('.')}' must be in Title Case.`,
      path: [...context.path], // Path to the violating element
    });
  }

  // Check for length
  const maxLength = options?.maxLength || 70; // Default to 70 if not provided
  if (targetVal.length > maxLength) {
    results.push({
      message: `Summary "${targetVal}" at path '${context.path.join('.')}' exceeds maximum length of ${maxLength} characters. Current length: ${targetVal.length}.`,
      path: [...context.path],
    });
  }

  return results;
};

Giải thích:

Bước 2: Biên dịch hàm TypeScript

Nếu bạn có kế hoạch sử dụng hàm này với bộ quy tắc .spectral.yaml hoặc .spectral.js trỏ tới một functionsDir, bạn sẽ cần biên dịch TypeScript của mình sang JavaScript.

Thêm một script build vào package.json của bạn:JSON

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

Chạy npm run build hoặc yarn build. Lệnh này sẽ biên dịch src/customFunctions.ts sang dist/customFunctions.js (dựa trên tsconfig.json của chúng ta).

Bước 3: Tạo tệp bộ quy tắc

Hãy tạo một bộ quy tắc, ví dụ: .spectral.js (hoặc .spectral.ts nếu bạn thích thiết lập hoàn toàn dựa trên TypeScript, xem phần tiếp theo).

Nếu sử dụng tệp bộ quy tắc JavaScript tham chiếu trực tiếp đến các hàm đã biên dịch:JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // Path to your compiled JS functions

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: 'Operation summaries must be title-cased and not exceed 70 characters.',
      message: '{{error}}', // The message will come from the custom function
      given: '$.paths[*][*].summary', // Targets all operation summaries
      severity: 'warn',
      formats: ["oas3"], // Apply only to OpenAPI v3 documents
      then: {
        function: titleCaseAndLength, // Directly reference the imported function
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // You can add more rules here
  },
};

Ngoài ra, nếu sử dụng .spectral.yamlfunctionsDir:

Đầu tiên, đảm bảo thư mục dist của bạn chứa một tệp index.js xuất các hàm của bạn, hoặc tệp customFunctions.js của bạn xuất chúng trực tiếp. Ví dụ, nếu dist/customFunctions.jsexports.titleCaseAndLength = ...;, bạn có thể làm như sau:YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # Directory containing compiled custom JS functions
rules:
  operation-summary-title-case-length:
    description: "Operation summaries must be title-cased and not exceed 70 characters."
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # Assuming functions are exported from customFunctions.js
      functionOptions:
        maxLength: 70

Ở đây, customFunctions#titleCaseAndLength cho Spectral biết hãy tìm tệp customFunctions.js (hoặc customFunctions/index.js) trong functionsDir và sử dụng hàm titleCaseAndLength được xuất.

Bước 4: Tạo tệp OpenAPI mẫu

Hãy tạo một tệp openapi.yaml đơn giản để kiểm thử quy tắc của chúng ta: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 # Incorrect: not title case
      responses:
        '200':
          description: A list of items.
    post:
      summary: Adds A New Item To The Ever Expanding Collection Of Items In The Store # Incorrect: too long
      responses:
        '201':
          description: Item created.
  /users:
    get:
      summary: Get User Details # Correct
      responses:
        '200':
          description: User details.

Bước 5: Chạy Spectral

Bây giờ, thực thi Spectral CLI đối với tài liệu OpenAPI của bạn:Bash

spectral lint openapi.yaml --ruleset .spectral.js
# or if using YAML ruleset (often auto-detected if named .spectral.yaml)
spectral lint openapi.yaml

Kết quả dự kiến:

Bạn sẽ thấy các cảnh báo tương tự như sau:

openapi.yaml
 2:10  warning  operation-summary-title-case-length  Summary "retrieves all items from the store" at path 'paths./items.get.summary' must be in Title Case.   paths./items.get.summary
 6:10  warning  operation-summary-title-case-length  Summary "Adds A New Item To The Ever Expanding Collection Of Items In The Store" at path 'paths./items.post.summary' exceeds maximum length of 70 characters. Current length: 78.  paths./items.post.summary

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

Kết quả này xác nhận rằng hàm TypeScript tùy chỉnh của chúng ta, được biên dịch sang JavaScript, đang xác định đúng các vi phạm.

Sử dụng Spectral theo lập trình với bộ quy tắc TypeScript

Đối với các kịch bản phức tạp hơn hoặc tích hợp chặt chẽ hơn vào các ứng dụng, bạn có thể muốn sử dụng Spectral theo lập trình và định nghĩa toàn bộ bộ quy tắc của mình bằng TypeScript. Điều này bỏ qua nhu cầu về tệp .spectral.yaml hoặc .spectral.js riêng biệt nếu muốn, và cho phép xây dựng bộ quy tắc động.

Tạo một tệp TypeScript cho công cụ lint của bạn, ví dụ src/linter.ts:TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // Example built-in function
import { titleCaseAndLength } from './customFunctions'; // Your custom TS function
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// Define the Spectral instance
const spectral = new Spectral();

// Load built-in rulesets and functions
spectral.setRuleset({
  extends: [[oas, 'recommended']], // Extends the recommended OpenAPI rules
  rules: {
    'operation-summary-title-case-length': {
      description: 'Operation summaries must be title-cased and not exceed 70 characters.',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // Directly use the TypeScript function
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // Example using a built-in function
        description: 'Info object should have a contact object.',
        message: 'API contact information is missing.',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // Using a built-in function
        },
      },
  },
});

// Function to lint a document
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // Create a Spectral Document object
    // The second argument (URI) is important for resolving relative $refs if any
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('Error linting document:', error);
    return [];
  }
}

// Example usage (e.g., in a script or another module)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // Path to your API spec
  if (diagnostics.length > 0) {
    console.log('API Linting Issues Found:');
    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('No API linting issues found. Great job!');
  }
}

// If you want to run this file directly with ts-node
if (require.main === module) {
  main().catch(console.error);
}

Để chạy mã này:Bash

npx ts-node src/linter.ts

Cách tiếp cận này cung cấp sự linh hoạt tối đa:

Các kỹ thuật hàm tùy chỉnh nâng cao

Hãy cùng khám phá một số khía cạnh nâng cao hơn của việc tạo các hàm tùy chỉnh.

Hàm tùy chỉnh bất đồng bộ:

Nếu hàm tùy chỉnh của bạn cần thực hiện các thao tác bất đồng bộ (ví dụ: lấy tài nguyên bên ngoài để xác thực dựa trên, mặc dù hãy sử dụng cẩn thận vì hiệu suất), bạn có thể định nghĩa nó như một hàm 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: `Resource '${targetVal}' not found at ${options.url}. Status: ${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `Error fetching resource '${targetVal}': ${error.message}` }];
  }
};

Spectral sẽ `await` đúng cách hàm bất đồng bộ của bạn.

Truy cập toàn bộ tài liệu và các giá trị đã được giải quyết:

Đối tượng context.document cho phép bạn truy cập toàn bộ tài liệu đã được phân tích cú pháp. Mạnh mẽ hơn, context.document.resolved cung cấp phiên bản đã được giải tham chiếu đầy đủ của tài liệu, điều này cần thiết khi xử lý các con trỏ $ref.TypeScript

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

// Example: Ensure a referenced schema has a specific property
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // This will be the object like { $ref: '#/components/schemas/MySchema' }
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // The `context.document.resolveAnchor` method can find the resolved value for a $ref
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `Could not resolve $ref: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `The schema referenced by "${targetVal.$ref}" at path '${context.path.join('.')}' must have property "${options.propertyName}".`,
      path: [...context.path, '$ref'] // Point to the $ref itself
    }];
  }
};

Hàm này sẽ được sử dụng với đường dẫn given nhắm mục tiêu các đối tượng chứa $ref, ví dụ: $.paths[*].*.responses.*.content.*.schema.

Làm việc với định dạng:

Đảm bảo các quy tắc tùy chỉnh của bạn chỉ định các định dạng mà chúng áp dụng (ví dụ: oas2, oas3, asyncapi2). Điều này ngăn quy tắc chạy trên các loại tài liệu không tương thích. Bạn có thể truy cập định dạng đã phát hiện trong hàm của mình thông qua context.document.formats.

Tổ chức và mở rộng bộ quy tắc

Khi tập hợp các quy tắc tùy chỉnh của bạn tăng lên, việc tổ chức trở nên quan trọng.

// .spectral.js (main ruleset)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... other rules or overrides
};

Tích hợp Spectral vào quy trình làm việc của bạn

Sức mạnh thực sự của việc lint API được hiện thực hóa khi nó được tự động hóa.

# Example GitHub Action step
- name: Lint API Specification
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

Các thực hành tốt nhất cho Spectral và các quy tắc tùy chỉnh bằng TypeScript

Kết luận: Nâng tầm quản trị API của bạn với Spectral và TypeScript

Spectral cung cấp một khung làm việc mạnh mẽ cho việc lint API, và bằng cách tận dụng TypeScript để phát triển quy tắc tùy chỉnh, bạn mang lại an toàn kiểu dữ liệu nâng cao, trải nghiệm nhà phát triển tốt hơn và khả năng bảo trì cho chiến lược quản trị API của mình. Dù bạn đang thực thi các thực hành tốt nhất của OpenAPI, các quy ước đặt tên riêng của công ty hay logic nghiệp vụ phức tạp trong thiết kế API của mình, sự kết hợp giữa công cụ quy tắc linh hoạt của Spectral và hệ thống gõ kiểu mạnh mẽ của TypeScript cung cấp một giải pháp mạnh mẽ.

Bằng cách tích hợp Spectral vào vòng đời phát triển của bạn, từ phản hồi từ IDE đến các pipeline CI/CD, bạn có thể đảm bảo API của mình nhất quán, chất lượng cao và tuân thủ các tiêu chuẩn đã định nghĩa của bạn, cuối cùng dẫn đến các API đáng tin cậy hơn và dễ sử dụng hơn. Hãy bắt đầu nhỏ, lặp lại trên bộ quy tắc của bạn và trao quyền cho các nhóm của bạn với các công cụ để xây dựng các API tốt hơn.

Thực hành thiết kế API trong Apidog

Khám phá cách dễ dàng hơn để xây dựng và sử dụng API