How to Use Spectral with TypeScript

This tutorial will guide you through the process of leveraging Spectral with TypeScript, from initial setup to crafting sophisticated custom validation logic.

Audrey Lopez

Audrey Lopez

19 May 2025

How to Use Spectral with TypeScript

Maintaining consistency, quality, and adherence to design standards is paramount for teams using APIs. API specifications like OpenAPI and AsyncAPI provide a blueprint, but ensuring these blueprints are followed correctly across numerous services and teams can be a daunting challenge. This is where API linting tools come into play, and Spectral stands out as a flexible and powerful open-source option. When combined with TypeScript, Spectral empowers developers to create robust, type-safe custom rules, elevating API governance to a new level.

This tutorial will guide you through the process of leveraging Spectral with TypeScript, from initial setup to crafting sophisticated custom validation logic. We'll explore how TypeScript enhances the development of Spectral rules, leading to more maintainable and reliable API linting solutions.

💡
Want a great API Testing tool that generates beautiful API Documentation?

Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?

Apidog delivers all your demans, and replaces Postman at a much more affordable price!
button

Understanding Spectral: The Guardian of Your API Specifications

Before diving into the TypeScript integration, let's establish what Spectral is and why it's a valuable tool in your API development toolkit.

Spectral is an open-source JSON/YAML linter with a primary focus on API description formats like OpenAPI (v2 and v3) and AsyncAPI. Its purpose is to help enforce API design guidelines, detect common errors, and ensure consistency across your API landscape. Think of it as ESLint or TSLint, but specifically for your API contracts.

Key Benefits of Using Spectral:

Spectral operates based on rulesets. A ruleset is a collection of rules, where each rule targets specific parts of your API document (using JSONPath expressions) and applies validation logic. Spectral comes with built-in rulesets (e.g., spectral:oas for OpenAPI standards), but its true power lies in the ability to define custom rulesets.

Why TypeScript for Spectral Custom Rules?

While Spectral rulesets can be defined in YAML or JavaScript (.js files), using TypeScript for developing custom functions offers significant advantages:

By writing your custom Spectral functions in TypeScript, you bring the same rigor and tooling benefits to your API governance code as you do to your application code.

Setting Up Your Spectral and TypeScript Environment

Let's get our hands dirty and set up the necessary tools.

Prerequisites:

Installation Steps:

First, you'll need the Spectral CLI to run linting operations and test your rules. It's often useful to install it globally or use npx.Bash

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

For developing custom rules programmatically and using Spectral's core libraries within a TypeScript project, install the necessary Spectral packages: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

Let's break down these packages:

Configure TypeScript:

Create a tsconfig.json file in your project root if you don't have one already. A basic configuration might look like this: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"]
}

Adjust outDir and rootDir according to your project structure. We'll assume your custom TypeScript functions will reside in the src directory.

Core Spectral Concepts: Rules, Rulesets, and Functions

Before writing TypeScript functions, let's solidify our understanding of Spectral's main components.

Rules:

A rule defines a specific check to be performed. Key properties of a rule include:

Rulesets:

A ruleset is a YAML or JavaScript file (e.g., .spectral.yaml, .spectral.js, or .spectral.ts when compiled) that groups rules. It can also:

Functions:

Functions are the core logic units that perform the actual validation. Spectral provides many built-in functions like:

The real power comes when these aren't enough, and you need to write your own custom functions – this is where TypeScript shines.

Crafting Your First Custom Spectral Function in TypeScript

Let's create a simple custom function. Imagine we want to enforce a rule that all API operation summaries must be title-cased and not exceed 70 characters.

Step 1: Define the Custom Function in TypeScript

Create a file, say 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;
};

Explanation:

Step 2: Compile the TypeScript Function

If you plan to use this function with a .spectral.yaml or .spectral.js ruleset that points to a functionsDir, you'll need to compile your TypeScript to JavaScript.

Add a build script to your package.json:JSON

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

Run npm run build or yarn build. This will compile src/customFunctions.ts to dist/customFunctions.js (based on our tsconfig.json).

Step 3: Create a Ruleset File

Let's create a ruleset, for example, .spectral.js (or .spectral.ts if you prefer a fully TypeScript-driven setup, see next section).

If using a JavaScript ruleset file directly referencing the compiled functions: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
  },
};

Alternatively, if using .spectral.yaml and functionsDir:

First, ensure your dist directory contains an index.js that exports your functions, or your customFunctions.js directly exports them. For instance, if dist/customFunctions.js has exports.titleCaseAndLength = ...;, you could do: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

Here, customFunctions#titleCaseAndLength tells Spectral to look for customFunctions.js (or customFunctions/index.js) in the functionsDir and use the exported titleCaseAndLength function.

Step 4: Create a Sample OpenAPI Document

Let's create a simple openapi.yaml file to test our rule: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.

Step 5: Run Spectral

Now, execute Spectral CLI against your OpenAPI document:Bash

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

Expected Output:

You should see warnings similar to this:

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)

This output confirms that our custom TypeScript function, compiled to JavaScript, is correctly identifying violations.

Programmatic Usage of Spectral with TypeScript Rulesets

For more complex scenarios or tighter integration into applications, you might want to use Spectral programmatically and define your ruleset entirely in TypeScript. This bypasses the need for a separate .spectral.yaml or .spectral.js file if desired, and allows for dynamic ruleset construction.

Create a TypeScript file for your linter, for example 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);
}

To run this:Bash

npx ts-node src/linter.ts

This approach provides maximum flexibility:

Advanced Custom Function Techniques

Let's explore some more advanced aspects of creating custom functions.

Asynchronous Custom Functions:

If your custom function needs to perform asynchronous operations (e.g., fetching an external resource to validate against, though use with caution for performance), you can define it as an async function.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 will correctly await your asynchronous function.

Accessing the Full Document and Resolved Values:

The context.document object gives you access to the entire parsed document. More powerfully, context.document.resolved provides the fully dereferenced version of the document, which is essential when dealing with $ref pointers.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
    }];
  }
};

This function would be used with a given path that targets objects containing $ref, for instance: $.paths[*].*.responses.*.content.*.schema.

Working with formats:

Ensure your custom rules specify the formats they apply to (e.g., oas2, oas3, asyncapi2). This prevents rules from running on incompatible document types. You can access the detected format within your function via context.document.formats.

Organizing and Extending Rulesets

As your collection of custom rules grows, organization becomes key.

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

Integrating Spectral into Your Workflow

The true power of API linting is realized when it's automated.

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

Best Practices for Spectral and TypeScript Custom Rules

Conclusion: Elevate Your API Governance with Spectral and TypeScript

Spectral offers a robust framework for API linting, and by leveraging TypeScript for custom rule development, you bring enhanced type safety, developer experience, and maintainability to your API governance strategy. Whether you're enforcing OpenAPI best practices, company-specific naming conventions, or complex business logic within your API designs, the combination of Spectral's flexible rule engine and TypeScript's strong typing system provides a powerful solution.

By integrating Spectral into your development lifecycle, from IDE feedback to CI/CD pipelines, you can ensure your APIs are consistent, high-quality, and adhere to your defined standards, ultimately leading to more reliable and easier-to-consume APIs. Start small, iterate on your rulesets, and empower your teams with the tools to build better APIs.

Explore more

How Much Does Claude API Cost in 2025

How Much Does Claude API Cost in 2025

Anthropic Claude has emerged as a powerful and versatile large language model (LLM), captivating developers and businesses with its advanced reasoning, creativity, and commitment to safety. As with any powerful tool, understanding the associated costs is paramount for effective implementation and sustainable innovation. This comprehensive tutorial will guide you through the intricacies of Claude API pricing, empowering you to make informed decisions and accurately forecast your expenses as you h

8 June 2025

How to Fix "Due to unexpected capacity constraints, Claude is unable to respond to your message" Error

How to Fix "Due to unexpected capacity constraints, Claude is unable to respond to your message" Error

It's a frustration familiar to many users of cutting-edge AI: you're deep in a productive workflow with Anthropic's Claude, crafting the perfect prompt, only to be met with the abrupt and unhelpful message: "Due to unexpected capacity constraints, Claude is unable to respond to your message. Please try again soon." This digital roadblock can halt creativity and productivity in their tracks, leaving users wondering what went wrong and how to get back on course. This comprehensive guide will delve

8 June 2025

A Developer's Guide: How to Generate API Specifications with Vercel v0 Workflows

A Developer's Guide: How to Generate API Specifications with Vercel v0 Workflows

In the fast-paced world of web development, efficiency and clarity are paramount. As projects grow in complexity, so does the need for well-defined APIs. A clear API specification acts as a contract between the frontend and backend, ensuring seamless communication and a smoother development process. But creating these specifications can be a tedious and time-consuming task. Enter Vercel's v0, an AI-powered tool designed to streamline the development workflow. While v0 is known for its ability t

7 June 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs