How to use spectral in typescript

In modern API development, consistency, quality, and adherence to standards are paramount. An API specification (like OpenAPI/Swagger) serves as the contract between producers and consumers. Ensuring this contract is clear, accurate, and follows best practices is crucial for smooth integration, development, and, importantly, testing. This is where API linting tools come into play. Spectral, developed by Stoplight, is a leading open-source linting tool designed specifically for JSON/YAML objects, with first-class support for API specifications like OpenAPI (v2 and v3) and AsyncAPI. It allows you to define and enforce rules on your API design documents, catching potential issues before they reach development or testing phases. While Spectral rulesets are often written in YAML, leveraging TypeScript unlocks powerful capabilities: type safety for complex rules, better maintainability, code reusability through custom functions, and a familiar environment for many developers. This tutorial will guide you through setting up and using Spectral with TypeScript, focusing on how this combination significantly enhances your API testing strategy by enabling a "shift-left" approach – catching contract and design issues early in the lifecycle. We will cover: Why Spectral and TypeScript? The benefits for API quality and testing. Setting up your TypeScript project for Spectral. Understanding Spectral's core concepts (Rulesets, Rules, given, then). Writing basic rules using TypeScript. Developing custom functions in TypeScript for complex validation logic. Applying Spectral specifically to improve API testability and contract validation. Integrating Spectral into your development workflow and CI/CD pipelines. Best practices for effective API linting. By the end, you'll understand how to build robust, maintainable, and type-safe linting rulesets using TypeScript to ensure your API specifications are top-notch, directly benefiting your API testing efforts. Why Spectral? Why TypeScript? Before diving in, let's understand the synergy between Spectral and TypeScript, especially from an API testing perspective. Benefits of Spectral: Consistency: Enforces naming conventions, structure, and style across all APIs within an organization. This consistency simplifies understanding, integration, and testing. Early Feedback (Shift-Left Testing): Catches errors, omissions, and design flaws in the API specification before any code is written or tests are executed. This is the earliest form of contract testing. Automation: Integrates seamlessly into CI/CD pipelines, automatically validating API specifications on every change. Collaboration: Provides a shared standard and vocabulary for API design discussions among developers, testers, and architects. Extensibility: Supports custom rules and functions to cater to specific organizational needs or complex validation logic. Benefits of using TypeScript with Spectral: Type Safety: TypeScript brings static typing to your custom rule functions. This catches errors during development (e.g., accessing non-existent properties, incorrect argument types) rather than at runtime when Spectral executes the rule. Enhanced Developer Experience (DX): Autocompletion, type hints, and compile-time checks in your IDE significantly speed up rule development and reduce errors. Complex Logic: While simple rules are fine in YAML, TypeScript excels when rules require complex conditional logic, external data fetching (use with caution!), or intricate data manipulation that's cumbersome in YAML. Code Reusability: Define common validation logic in TypeScript functions and reuse them across multiple rules or even multiple rulesets. Maintainability: For large or complex rulesets, TypeScript's modularity and typing make the codebase easier to understand, refactor, and maintain over time. Familiarity: Teams already using TypeScript for their applications can leverage their existing skills and tooling. The API Testing Connection: Using Spectral (especially with TypeScript) directly impacts API testing by: Validating the Contract: Ensuring the OpenAPI spec is syntactically correct and adheres to defined standards is the first step of contract testing. It guarantees the document consumers (developers, testers, tools) rely on is sound. Improving Testability: Rules can enforce the presence of clear descriptions, meaningful operationIds, and well-defined examples, all of which make writing and understanding automated tests much easier. Preventing Common Pitfalls: Rules can check for missing error definitions, inconsistent pagination parameters, insecure security scheme usage, etc., preventing entire classes of bugs that would otherwise need to be caught during functional or integration testing. Ensuring Example Validity: Custom rules can check if response/request examples actually conform to their defined schemas – crucial for generating reliable mock servers or contract t

Apr 19, 2025 - 09:27
 0
How to use spectral in typescript

In modern API development, consistency, quality, and adherence to standards are paramount. An API specification (like OpenAPI/Swagger) serves as the contract between producers and consumers. Ensuring this contract is clear, accurate, and follows best practices is crucial for smooth integration, development, and, importantly, testing.

This is where API linting tools come into play. Spectral, developed by Stoplight, is a leading open-source linting tool designed specifically for JSON/YAML objects, with first-class support for API specifications like OpenAPI (v2 and v3) and AsyncAPI. It allows you to define and enforce rules on your API design documents, catching potential issues before they reach development or testing phases.

While Spectral rulesets are often written in YAML, leveraging TypeScript unlocks powerful capabilities: type safety for complex rules, better maintainability, code reusability through custom functions, and a familiar environment for many developers.

This tutorial will guide you through setting up and using Spectral with TypeScript, focusing on how this combination significantly enhances your API testing strategy by enabling a "shift-left" approach – catching contract and design issues early in the lifecycle.

We will cover:

  1. Why Spectral and TypeScript? The benefits for API quality and testing.
  2. Setting up your TypeScript project for Spectral.
  3. Understanding Spectral's core concepts (Rulesets, Rules, given, then).
  4. Writing basic rules using TypeScript.
  5. Developing custom functions in TypeScript for complex validation logic.
  6. Applying Spectral specifically to improve API testability and contract validation.
  7. Integrating Spectral into your development workflow and CI/CD pipelines.
  8. Best practices for effective API linting.

By the end, you'll understand how to build robust, maintainable, and type-safe linting rulesets using TypeScript to ensure your API specifications are top-notch, directly benefiting your API testing efforts.

Why Spectral? Why TypeScript?

Before diving in, let's understand the synergy between Spectral and TypeScript, especially from an API testing perspective.

Benefits of Spectral:

  1. Consistency: Enforces naming conventions, structure, and style across all APIs within an organization. This consistency simplifies understanding, integration, and testing.
  2. Early Feedback (Shift-Left Testing): Catches errors, omissions, and design flaws in the API specification before any code is written or tests are executed. This is the earliest form of contract testing.
  3. Automation: Integrates seamlessly into CI/CD pipelines, automatically validating API specifications on every change.
  4. Collaboration: Provides a shared standard and vocabulary for API design discussions among developers, testers, and architects.
  5. Extensibility: Supports custom rules and functions to cater to specific organizational needs or complex validation logic.

Benefits of using TypeScript with Spectral:

  1. Type Safety: TypeScript brings static typing to your custom rule functions. This catches errors during development (e.g., accessing non-existent properties, incorrect argument types) rather than at runtime when Spectral executes the rule.
  2. Enhanced Developer Experience (DX): Autocompletion, type hints, and compile-time checks in your IDE significantly speed up rule development and reduce errors.
  3. Complex Logic: While simple rules are fine in YAML, TypeScript excels when rules require complex conditional logic, external data fetching (use with caution!), or intricate data manipulation that's cumbersome in YAML.
  4. Code Reusability: Define common validation logic in TypeScript functions and reuse them across multiple rules or even multiple rulesets.
  5. Maintainability: For large or complex rulesets, TypeScript's modularity and typing make the codebase easier to understand, refactor, and maintain over time.
  6. Familiarity: Teams already using TypeScript for their applications can leverage their existing skills and tooling.

The API Testing Connection:

Using Spectral (especially with TypeScript) directly impacts API testing by:

  • Validating the Contract: Ensuring the OpenAPI spec is syntactically correct and adheres to defined standards is the first step of contract testing. It guarantees the document consumers (developers, testers, tools) rely on is sound.
  • Improving Testability: Rules can enforce the presence of clear descriptions, meaningful operationIds, and well-defined examples, all of which make writing and understanding automated tests much easier.
  • Preventing Common Pitfalls: Rules can check for missing error definitions, inconsistent pagination parameters, insecure security scheme usage, etc., preventing entire classes of bugs that would otherwise need to be caught during functional or integration testing.
  • Ensuring Example Validity: Custom rules can check if response/request examples actually conform to their defined schemas – crucial for generating reliable mock servers or contract tests.

Setting Up Your TypeScript Project for Spectral

Let's set up a Node.js project using TypeScript to write our Spectral rules.

Prerequisites:

  • Node.js (v14 or later recommended)
  • npm or yarn

Steps:

  1. Initialize Project:

    mkdir spectral-typescript-tutorial
    cd spectral-typescript-tutorial
    npm init -y
    
  2. Install Dependencies:
    We need Spectral's core library, the CLI for running it, format definitions (like OpenAPI), built-in functions, TypeScript, and Node types.

    npm install @stoplight/spectral-core @stoplight/spectral-cli @stoplight/spectral-formats @stoplight/spectral-functions typescript @types/node --save-dev
    # Or using yarn:
    # yarn add @stoplight/spectral-core @stoplight/spectral-cli @stoplight/spectral-formats @stoplight/spectral-functions typescript @types/node --dev
    
  3. Configure TypeScript (tsconfig.json):
    Create a tsconfig.json file in the project root:

    // tsconfig.json
    {
      "compilerOptions": {
        "target": "ES2016", // Spectral generally runs on Node, ES2016+ is safe
        "module": "CommonJS", // Spectral typically uses CommonJS for rulesets
        "outDir": "./dist", // Output directory for compiled JS
        "rootDir": "./src", // Source directory for TS files
        "strict": true, // Enable all strict type-checking options
        "esModuleInterop": true, // Allows default imports from commonjs modules
        "skipLibCheck": true, // Skip type checking of declaration files
        "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references
        "moduleResolution": "node", // Specify module resolution strategy
        "resolveJsonModule": true // Allows importing JSON files
      },
      "include": ["src/**/*.ts"], // Which files to include
      "exclude": ["node_modules", "dist"] // Which files/dirs to exclude
    }
    
  4. Create Source Directory:

    mkdir src
    
  5. Create a Sample API Specification:
    Let's create a basic OpenAPI 3 specification file to lint. Save this as sample-api.yaml in the project root.

    # sample-api.yaml
    openapi: 3.0.0
    info:
      title: Sample Pet Store API
      version: 1.0.0
      description: A basic API for managing pets
    paths:
      /pets:
        get:
          summary: List all pets
          operationId: listPets
          parameters:
            - name: limit # Potential issue: inconsistent case
              in: query
              required: false
              schema:
                type: integer
                format: int32
                minimum: 1
                maximum: 100
            - name: next_token # Potential issue: inconsistent case
              in: query
              required: false
              schema:
                type: string
          responses:
            '200':
              description: A list of pets.
              content:
                application/json:
                  schema:
                    type: array
                    items:
                      $ref: '#/components/schemas/Pet'
            '400': # Potential issue: Missing description
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Error'
      /pets/{petId}:
        get:
          summary: Info for a specific pet
          operationId: showPetById
          parameters:
            - name: petId
              in: path
              required: true
              description: The id of the pet to retrieve
              schema:
                type: string
          responses:
            '200':
              description: Information about the pet
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Pet'
            default: # Good practice: default error response
              description: unexpected error
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Error'
    
    components:
      schemas:
        Pet:
          type: object
          required:
            - id
            - name
          properties:
            id:
              type: integer
              format: int64
            name:
              type: string
            tag:
              type: string
        Error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: integer
              format: int32
            message:
              type: string
    

Now our project structure looks roughly like this:

spectral-typescript-tutorial/
├── node_modules/
├── src/
├── package.json
├── package-lock.json (or yarn.lock)
├── sample-api.yaml
└── tsconfig.json

Understanding Spectral Core Concepts

Before writing rules in TypeScript, let's grasp Spectral's fundamentals:

  • Ruleset: A file (e.g., .spectral.ts, .spectral.js, .spectral.yaml) that defines the linting rules, configurations, and potentially extends other rulesets. We'll use .spectral.ts.
  • Rules: Individual checks applied to the API specification. Each rule has:
    • description: Explains what the rule checks for.
    • message: The error/warning message shown when the rule fails (can use placeholders).
    • severity: How critical the issue is (error, warn, info, hint). error typically fails CI builds.
    • given: One or more JSONPath expressions specifying which parts of the document the rule applies to.
    • then: The core logic. It specifies a function (or an array of functions) to execute against the values selected by given. Spectral provides built-in functions (truthy, pattern, schema, alphabetical, etc.), and we can create custom ones in TypeScript.
    • formats: Specifies which document types the rule applies to (e.g., [oas3] for OpenAPI 3).

Creating Your First Ruleset in TypeScript

Let's create our main ruleset file: src/spectral.ts.

// src/spectral.ts
import { oas3 } from '@stoplight/spectral-formats'; // Import OpenAPI 3 format
import { truthy, pattern } from '@stoplight/spectral-functions'; // Import built-in functions

// Define the ruleset using Spectral's type definitions (though not strictly required, it helps with DX)
// We can import types if needed, e.g. import { RulesetDefinition } from '@stoplight/spectral-core';

export default {
  // extends: [], // You can extend existing rulesets (e.g., spectral:oas)
  rules: {
    'info-description-present': {
      description: 'API Info block should have a description.',
      message: 'API `info.description` is missing.',
      given: '$.info', // Target the info object
      then: {
        field: 'description', // Check the 'description' field within the info object
        function: truthy // Use the built-in 'truthy' function to ensure it exists and is not empty/false
      },
      severity: 'warn', // Severity level
      formats: [oas3] // Apply only to OpenAPI 3 documents
    },
    'operation-id-kebab-case': {
      description: 'Operation IDs should be kebab-case.',
      message: 'Operation ID `{{value}}` is not kebab-case. Example: `list-pets`.',
      given: '$.paths[*][*].operationId', // Target all operationIds
      then: {
        function: pattern, // Use the built-in 'pattern' function
        functionOptions: {
          match: '^[a-z][a-z0-9-]*$' // Regex for kebab-case
        }
      },
      severity: 'error',
      formats: [oas3]
    },
    'error-response-has-description': {
        description: 'Error responses (4xx, 5xx) must have a description.',
        message: 'Response {{property}} ({{path}}) must have a description.',
        given: "$.paths[*][*].responses[?(@property.match(/^[45]/))]", // Target responses with 4xx or 5xx status codes
        then: {
          field: 'description',
          function: truthy
        },
        severity: 'warn',
        formats: [oas3]
    }
  }
};

Explanation:

  1. We import the oas3 format specifier and some built-in functions (truthy, pattern).
  2. We export a default object representing our ruleset.
  3. info-description-present: Checks if the $.info.description field exists and is truthy. Uses given: '$.info' and then.field: 'description'.
  4. operation-id-kebab-case: Checks if all operationId values match the kebab-case pattern ^[a-z][a-z0-9-]*$. Uses given to target all operation IDs and the pattern function. Note the {{value}} placeholder in the message.
  5. error-response-has-description: Checks if responses starting with 4 or 5 (like 400, 503) have a description. It uses a JSONPath filter ?(@property.match(/^[45]/)) to select specific response codes. Note {{property}} (the response code) and {{path}} placeholders.

Compiling and Running:

  1. Compile TypeScript to JavaScript:

    npx tsc
    

    This command reads tsconfig.json and compiles src/spectral.ts into dist/spectral.js.

  2. Run Spectral CLI:
    Use the Spectral CLI to lint our sample-api.yaml using the compiled ruleset.

    npx spectral lint ./sample-api.yaml --ruleset ./dist/spectral.js
    

Expected Output:

You should see output similar to this, highlighting the issues in sample-api.yaml:

./sample-api.yaml
 12:18  error    operation-id-kebab-case  Operation ID `listPets` is not kebab-case. Example: `list-pets`.   paths./pets.get.operationId
 29:14  warning  error-response-has-description  Response 400 (paths./pets.get.responses.400) must have a description.  paths./pets.get.responses.400
 40:18  error    operation-id-kebab-case  Operation ID `showPetById` is not kebab-case. Example: `show-pet-by-id`.  paths./pets/{petId}.get.operationId

✖ 3 problems (2 errors, 1 warning, 0 infos, 0 hints)

This output clearly shows the violations based on our TypeScript ruleset: the operationIds are not kebab-case, and the 400 response is missing a description.

Leveraging TypeScript for Custom Functions

The real power of using TypeScript comes with custom functions for logic that built-in functions can't handle easily.

Let's create a rule to enforce that query parameters use snake_case. The built-in pattern function works on the value of a field, but here we need to check the name of the parameter objects within the parameters array.

  1. Create a Custom Functions File:
    Create src/functions.ts:

    // src/functions.ts
    import { IFunction, IFunctionResult } from '@stoplight/spectral-core';
    
    // Custom function to check if query parameter names are snake_case
    export const queryParamIsSnakeCase: IFunction = (targetVal, opts, context): IFunctionResult[] => {
      const results: IFunctionResult[] = [];
    
      // targetVal here is the array of parameters for an operation
      if (!Array.isArray(targetVal)) {
        return results; // Should be an array, ignore if not
      }
    
      for (let i = 0; i < targetVal.length; i++) {
        const param = targetVal[i];
        // Only check query parameters that have a 'name' property
        if (param && param.in === 'query' && typeof param.name === 'string') {
          const paramName = param.name;
          // Basic snake_case regex: starts with lowercase, contains lowercase, digits, and underscores
          const snakeCaseRegex = /^[a-z][a-z0-9_]*$/;
    
          if (!snakeCaseRegex.test(paramName)) {
            results.push({
              message: `Query parameter "${paramName}" is not snake_case. Example: 'next_token'.`,
              // Provide the path to the specific parameter name within the array
              path: [...context.path, i, 'name']
            });
          }
        }
      }
    
      return results;
    };
    
    // Add more custom functions here as needed...
    

    Explanation:

    • We import IFunction (the type for a Spectral function) and IFunctionResult (the type for the results it returns).
    • The function receives targetVal (the value selected by given), opts (options passed from the rule), and context (information about the path and document).
    • It iterates through the parameters array (targetVal).
    • For each parameter where in is query, it checks if name matches the snakeCaseRegex.
    • If it doesn't match, it pushes an error object onto the results array. Crucially, it specifies the exact path to the offending parameter name using context.path and appending the array index and field name (i, 'name').
  2. Update the Ruleset (src/spectral.ts):
    Import and use the custom function.

    // src/spectral.ts
    import { oas3 } from '@stoplight/spectral-formats';
    import { truthy, pattern } from '@stoplight/spectral-functions';
    // Import our custom function
    import { queryParamIsSnakeCase } from './functions'; // Adjust path if needed
    
    export default {
      // extends: [],
      rules: {
        // ... (previous rules: info-description-present, operation-id-kebab-case, error-response-has-description) ...
    
        'query-param-snake-case': {
          description: 'Query parameters must use snake_case.',
          // Message is now generated by the custom function
          // message: '...',
          given: '$.paths[*][*].parameters', // Target the parameters array for each operation
          then: {
            function: queryParamIsSnakeCase // Use our custom function!
          },
          severity: 'error',
          formats: [oas3]
        }
      }
    };
    
  3. Recompile and Run:

    npx tsc
    npx spectral lint ./sample-api.yaml --ruleset ./dist/spectral.js
    

New Expected Output:

You should now see an additional error related to the limit parameter:

./sample-api.yaml
 12:18  error    operation-id-kebab-case   Operation ID `listPets` is not kebab-case. Example: `list-pets`.  paths./pets.get.operationId
 14:20  error    query-param-snake-case    Query parameter "limit" is not snake_case. Example: 'next_token'.   paths./pets.get.parameters.0.name
 29:14  warning  error-response-has-description  Response 400 (paths./pets.get.responses.400) must have a description. paths./pets.get.responses.400
 40:18  error    operation-id-kebab-case   Operation ID `showPetById` is not kebab-case. Example: `show-pet-by-id`. paths./pets/{petId}.get.operationId

✖ 4 problems (3 errors, 1 warning, 0 infos, 0 hints)

Our custom TypeScript function successfully identified the non-snake_case query parameter limit.

Spectral for API Testing: Shifting Left with Custom Rules

Now let's focus explicitly on rules that directly aid API testing by catching contract issues and improving testability.

Example 1: Ensure Response Examples Validate Against Schema

Valid examples are crucial for documentation, mock servers, and contract testing tools (like Dredd or Pact). A common issue is examples drifting out of sync with their schemas.

  • Concept: We can write a custom function that uses a JSON Schema validator (like AJV, which Spectral often bundles internally or can be added) to validate examples or example fields against the corresponding schema.

  • Simplified Implementation Idea (Conceptual - Requires Schema Validation Logic):

    // src/functions.ts (Conceptual - requires schema validation setup)
    import { IFunction, IFunctionResult } from '@stoplight/spectral-core';
    // Assume Ajv is available or add it: npm install ajv --save-dev
    import Ajv from 'ajv';
    
    const ajv = new Ajv(); // Basic instance
    
    export const exampleMatchesSchema: IFunction = (targetVal, opts, context): IFunctionResult[] => {
        const results: IFunctionResult[] = [];
        // targetVal is likely the response or request body object containing 'schema' and 'example'/'examples'
    
        if (!targetVal || !targetVal.schema || (!targetVal.example && !targetVal.examples)) {
            return results; // No schema or example to validate
        }
    
        const schema = targetVal.schema; // This might need resolving ($ref) depending on context
    
        // Helper to validate a single example
        const validateExample = (exampleValue: any, examplePath: (string | number)[]) => {
            // *** Important: Schema resolution ($ref) might be needed here! ***
            // Spectral's context or dedicated functions might help resolve refs.
            // This simplified example assumes schema is fully resolved.
            try {
                const validate = ajv.compile(schema);
                const valid = validate(exampleValue);
                if (!valid) {
                    results.push({
                        message: `Example does not match schema: ${ajv.errorsText(validate.errors)}`,
                        path: examplePath
                    });
                }
            } catch (error: any) {
                 results.push({ // Catch schema compilation errors too
                    message: `Schema validation error: ${error.message}`,
                    path: [...context.path, 'schema'] // Point to the schema causing issues
                 });
            }
        };
    
        // Check single 'example'
        if (targetVal.example) {
            validateExample(targetVal.example, [...context.path, 'example']);
        }
    
        // Check multiple 'examples'
        if (targetVal.examples) {
            for (const key in targetVal.examples) {
                if (targetVal.examples[key]?.value) {
                    validateExample(targetVal.examples[key].value, [...context.path, 'examples', key, 'value']);
                }
            }
        }
    
        return results;
    };
    

    Integration in src/spectral.ts:

    // src/spectral.ts
    // ... imports ...
    import { exampleMatchesSchema } from './functions'; // Add import
    
    export default {
        // ...
        rules: {
            // ... other rules ...
            'response-example-matches-schema': {
                description: 'Response examples should be valid according to their schema.',
                given: "$.paths[*][*].responses[*].content[*]", // Target content objects with schema/examples
                then: {
                    function: exampleMatchesSchema
                },
                severity: 'warn', // Warning because examples aren't always required
                formats: [oas3]
            },
            'request-body-example-matches-schema': {
                description: 'Request body examples should be valid according to their schema.',
                given: "$.paths[*][*].requestBody.content[*]", // Target request body content objects
                then: {
                    function: exampleMatchesSchema
                },
                severity: 'warn',
                formats: [oas3]
            }
        }
    }
    

    (Note: Full schema resolution ($ref handling) can be complex and might require leveraging more advanced Spectral context features or helper libraries.)

Example 2: Enforce Security Scheme Definition for Modifying Operations

Ensure that operations that modify data (POST, PUT, PATCH, DELETE) declare a security requirement. This is crucial for testing secured endpoints correctly. Unprotected modification endpoints are a major security risk.

  • Concept: Target operations using specific HTTP methods and check if the security field is present and non-empty at the operation level or inherited from the global level.

  • Implementation (using built-in functions and JSON Path):

    // src/spectral.ts
    import { oas3 } from '@stoplight/spectral-formats';
    import { truthy, pattern, defined } from '@stoplight/spectral-functions'; // Add 'defined'
    import { queryParamIsSnakeCase, exampleMatchesSchema } from './functions'; // Add previous custom functions
    
    export default {
      // extends: [], // Consider extending spectral:oas for basic OAS rules
      rules: {
        // ... (info-description-present, operation-id-kebab-case, error-response-has-description, query-param-snake-case, response-example-matches-schema, request-body-example-matches-schema) ...
    
        'modifying-operation-requires-security': {
          description: 'Operations that modify data (POST, PUT, PATCH, DELETE) must have a security requirement defined.',
          message: 'The {{path}} operation modifies data but does not specify a security requirement.',
          // Target operations with these methods specifically
          given: "$.paths[*][?( @property === 'post' || @property === 'put' || @property === 'patch' || @property === 'delete' )]",
          then: {
            // Check if the 'security' field is defined on the operation itself
            // Note: Spectral automatically considers global security if operation-level is undefined.
            // So, checking for 'defined' covers both cases implicitly for standard tools.
            // A stricter check might involve resolving global security explicitly if needed.
            field: 'security',
            function: defined // The 'defined' function checks if the key exists.
                             // For stricter check (non-empty array), a custom function might be better.
          },
          severity: 'error', // Security issues are usually errors
          formats: [oas3]
        }
      }
    };
    

    Explanation:

    • The given path targets path items ($.paths[*]) and then filters the HTTP methods ([?( @property === 'post' || ... )]) to select only POST, PUT, PATCH, or DELETE operations.
    • The then clause checks if the security field is defined for that operation. Spectral's resolution logic usually means if it's not defined locally, it looks globally ($.security). The defined function is a simple check; a more robust check might ensure security is a non-empty array, potentially using a custom function if complex logic (like checking specific scheme types) is needed.
    • This rule helps testers ensure they are considering authentication/authorization when planning and executing tests for data-modifying endpoints.

Example 3: Ensure Meaningful and Unique Operation IDs

operationId is often used by code generators, SDKs, and testing frameworks to create function names or test case identifiers. Ensuring they are present, unique, and descriptive improves test generation and clarity.

  • Concept: Check that operationId exists for every operation and potentially enforce uniqueness (Spectral has built-in uniqueness checks).

  • Implementation (using built-in functions):

    // src/spectral.ts
    // ... imports ...
    import { alphabetical, length, nunique } from '@stoplight/spectral-functions'; // Add 'length', 'nunique'
    
    export default {
        // ...
        rules: {
            // ... other rules ...
    
            'operation-id-present': {
                description: 'All operations must have an operationId.',
                message: 'Operation {{path}} is missing an operationId.',
                given: '$.paths[*][*]', // Target every operation object
                then: {
                    field: 'operationId',
                    function: truthy // Ensure it exists and is not empty
                },
                severity: 'error',
                formats: [oas3]
            },
    
            'operation-id-unique': {
                description: 'All operationIds must be unique across the API specification.',
                message: 'OperationId "{{value}}" is not unique. Found duplicates.',
                // This 'given' collects ALL operationIds into a single array for uniqueness check
                given: '$..operationId',
                then: {
                  function: nunique // Built-in function to check uniqueness in an array
                },
                severity: 'error',
                formats: [oas3]
            },
             // Optional: Enforce a minimum length for better description?
            'operation-id-min-length': {
                description: 'Operation IDs should be descriptive (enforce min length).',
                message: 'Operation ID "{{value}}" is too short. Minimum length is 5.',
                given: '$..operationId', // Target all operationId values
                then: {
                    function: length,
                    functionOptions: {
                        min: 5
                    }
                },
                severity: 'hint', // Less critical, maybe a hint or warning
                formats: [oas3]
            }
        }
    }
    

    Explanation:

    • operation-id-present: Ensures every operation has an operationId.
    • operation-id-unique: Uses the special .. recursive descent JSONPath syntax ($..operationId) to gather all operationId values anywhere in the document into a single list, then applies the nunique function to ensure there are no duplicates.
    • operation-id-min-length (Optional): Uses the length function to check if the string value of the operationId meets a minimum length, nudging towards more descriptive names.

Example 4: Consistent Pagination Parameters

Inconsistent pagination (e.g., using limit/offset in one endpoint and pageSize/pageToken in another) complicates client implementation and testing.

  • Concept: Define the standard pagination parameters (e.g., limit, offset or page_size, page_token) and ensure all collection GET endpoints use them consistently.

  • Implementation (Custom Function Required): This requires a custom function because we need to check for the presence of specific parameter names within the parameters array of relevant operations.

    // src/functions.ts
    // ... other functions ...
    import { IFunction, IFunctionResult } from '@stoplight/spectral-core';
    
    export const checkConsistentPaginationParams: IFunction = (targetVal, opts, context): IFunctionResult[] => {
        const results: IFunctionResult[] = [];
        // targetVal here is the operation object (e.g., $.paths['/items'].get)
        // opts should contain the expected parameter names, e.g., { expectedParams: ['limit', 'offset'] }
    
        if (!targetVal || !opts?.expectedParams || !Array.isArray(opts.expectedParams)) {
            return results; // Invalid input or options
        }
    
        // Check only GET operations that likely return collections (heuristic: path doesn't end with '}')
        const currentPath = context.path[context.path.length - 2]?.toString() ?? ''; // Get the path string like '/pets'
        const isCollectionPath = !currentPath.endsWith('}');
        const isGetOperation = context.path[context.path.length - 1] === 'get';
    
        if (!isGetOperation || !isCollectionPath) {
             return results; // Only check GET on potential collection paths
        }
    
        const parameters: any[] = targetVal.parameters || [];
        const paramNames = new Set(parameters.map(p => p?.name).filter(name => typeof name === 'string'));
    
        const expectedParams = new Set(opts.expectedParams as string[]);
        let hasAnyExpectedParam = false;
        let hasUnexpectedPaginationParam = false;
    
        // Check if *any* expected param is present
        for (const expected of expectedParams) {
            if (paramNames.has(expected)) {
                hasAnyExpectedParam = true;
                break;
            }
        }
    
        // Define potentially conflicting params (adjust based on your common anti-patterns)
        const conflictingParams = ['page', 'size', 'pageSize', 'pageToken', 'cursor'];
        for (const conflict of conflictingParams) {
            if (!expectedParams.has(conflict) && paramNames.has(conflict)) {
                 hasUnexpectedPaginationParam = true;
                 results.push({
                    message: `Operation uses potentially conflicting/unexpected pagination parameter "${conflict}". Expected convention: ${opts.expectedParams.join(', ')}.`,
                    path: [...context.path, 'parameters'] // Path to the parameters array
                 });
            }
        }
    
        // If it looks like a collection GET, but has no expected pagination params (and no conflicting ones were found yet)
        // Be careful: some collections might not be paginated. This rule might need refinement.
        if (isCollectionPath && isGetOperation && !hasAnyExpectedParam && !hasUnexpectedPaginationParam) {
            // This check is optional and potentially noisy. Only add if strict pagination is required everywhere.
            // results.push({
            //    message: `Operation appears to return a collection but is missing standard pagination parameters (${opts.expectedParams.join(', ')}).`,
            //    path: context.path
            // });
        }
    
        return results;
    };
    

    Integration in src/spectral.ts:

    // src/spectral.ts
    // ... imports ...
    import { checkConsistentPaginationParams } from './functions'; // Add import
    
    export default {
        // ...
        rules: {
            // ... other rules ...
    
            'consistent-pagination': {
                description: 'GET operations on collections should use consistent pagination parameters (e.g., limit, offset).',
                // Message is provided by the custom function
                given: "$.paths[*].get", // Target all GET operations
                then: {
                    function: checkConsistentPaginationParams,
                    functionOptions: {
                        // Define YOUR standard pagination parameters here
                        expectedParams: ['limit', 'offset']
                        // Or use: expectedParams: ['page_size', 'page_token']
                    }
                },
                severity: 'warn', // Might be a warning depending on API strategy
                formats: [oas3]
            }
        }
    }
    

    This custom function checkConsistentPaginationParams looks at GET operations on paths likely representing collections. It checks if they use parameters other than the expected ones (limit, offset in this case) or known conflicting ones. It highlights inconsistencies, helping ensure predictable behavior for clients and simpler test setup.

Integrating Spectral into Your Workflow and CI/CD

Linting is most effective when automated. Here's how to integrate Spectral:

  1. Local Development:

    • Git Hook: Use tools like husky and lint-staged to automatically run Spectral on staged API specification files (*.yaml, *.json) before committing. This provides immediate feedback to the developer.

      npm install husky lint-staged --save-dev
      npx husky install
      # Add to package.json scripts: "prepare": "husky install"
      # Create .husky/pre-commit hook:
      # npx husky add .husky/pre-commit "npx lint-staged"
      
      # Add to package.json:
      # "lint-staged": {
      #   "*.{yaml,yml,json}": [ // Adjust pattern for your spec files
      #     "npx tsc", # Ensure rules are compiled
      #     "npx spectral lint --ruleset ./dist/spectral.js"
      #   ]
      # }
      
*   **IDE Integration:** Some IDEs have extensions or plugins that can run Spectral in the background (e.g., VS Code extensions), providing real-time feedback.
  1. CI/CD Pipeline (e.g., GitHub Actions, GitLab CI, Jenkins):

    • Add a stage/job to your pipeline that runs after code checkout but before deployment or extensive testing.
    • This job should:
      1. Checkout the code.
      2. Set up Node.js.
      3. Install dependencies (npm ci or yarn install).
      4. Compile the TypeScript ruleset (npx tsc).
      5. Run Spectral against your API specification file(s) (npx spectral lint ...).
      6. Fail the build if Spectral reports errors (warnings might be optional). The exit code of spectral lint reflects this (0 for success/only warnings, non-zero for errors).

    Example GitHub Action Step:

    name: API Spec Lint
    
    on: [push, pull_request]
    
    jobs:
      lint-api-spec:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - name: Set up Node.js
            uses: actions/setup-node@v3
            with:
              node-version: '18' # Or your preferred version
              cache: 'npm'
          - name: Install dependencies
            run: npm ci
          - name: Compile Spectral Ruleset
            run: npx tsc
          - name: Lint API Specification
            # Adjust path to your spec file and ruleset
            run: npx spectral lint ./path/to/your/api-spec.yaml --ruleset ./dist/spectral.js --fail-severity=error
    

Best Practices for Spectral + TypeScript

  • Start Simple, Extend Gradually: Begin with a core ruleset (like spectral:oas) and add custom rules incrementally as needed. Don't try to enforce everything at once.
  • Clear Rule Descriptions and Messages: Make it obvious why a rule exists and how to fix the violation. Use placeholders ({{value}}, {{path}}, {{property}}) effectively.
  • Use Appropriate Severities: Distinguish between critical errors (error) that should block processes and stylistic suggestions or warnings (warn, info, hint).
  • Keep Custom Functions Focused: Each custom function should perform a single, well-defined check. This improves reusability and testability (yes, you can unit test your custom functions!).
  • Leverage TypeScript Types: Use IFunction, IFunctionResult, ISpectralDiagnostic and other types from @stoplight/spectral-core for better type safety and autocompletion in your custom functions.
  • Document Your Ruleset: Add comments in your .spectral.ts file explaining complex rules or design decisions.
  • Version Control Your Ruleset: Treat your .spectral.ts file and custom functions like any other code artifact – store it in Git, review changes, etc.
  • Consider Shared Rulesets: For larger organizations, create a shared npm package containing common TypeScript functions and base rulesets that individual projects can extend.

Conclusion

Spectral, supercharged with TypeScript, provides a robust framework for enforcing API specification quality and consistency. By defining rules – from simple pattern checks to complex validations using custom, type-safe functions – you shift quality checks to the earliest stage of the API lifecycle: the design phase.

From an API testing perspective, this is invaluable. Linting the API specification acts as the foundational layer of contract testing. It ensures the document that guides developers, testers, and consumers is accurate, complete, testable, and adheres to agreed-upon standards. Catching inconsistencies in naming, missing error definitions, insecure operations, or invalid examples before they manifest as bugs in code saves significant time and effort during testing and integration.

By integrating Spectral with TypeScript into your development workflow and CI/CD pipelines, you build a safety net that promotes better API design, reduces friction between teams, and ultimately leads to higher-quality, more reliable, and easier-to-test APIs. Start linting today!