Apidog

All-in-one Collaborative API Development Platform

API Design

API Documentation

API Debugging

API Mocking

API Automated Testing

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.

Mikael Svenson

Mikael Svenson

Updated on May 19, 2025

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:

  • Consistency: Enforces uniform API design across teams and projects.
  • Quality Assurance: Catches errors and bad practices early in the development lifecycle.
  • Improved Collaboration: Provides a shared understanding of API standards.
  • Automation: Integrates seamlessly into CI/CD pipelines for automated validation.
  • Extensibility: Allows for the creation of custom rules tailored to specific organizational needs.
  • Format Agnostic Core: While excelling at OpenAPI/AsyncAPI, its core can lint any JSON/YAML structure.

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:

  • Type Safety: TypeScript's static typing catches errors at compile-time, reducing runtime surprises in your custom linting logic. This is crucial for complex rules.
  • Improved Developer Experience: Autocompletion, refactoring capabilities, and better code navigation in IDEs make writing and maintaining custom functions easier.
  • Enhanced Readability and Maintainability: Explicit types make the intent and structure of your custom functions clearer, especially for teams.
  • Modern JavaScript Features: Utilize modern ES features with confidence, as TypeScript compiles down to compatible JavaScript.
  • Better Testability: Typing makes it easier to write robust unit tests for your custom Spectral functions.

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:

  • Node.js and npm (or yarn): Spectral is a Node.js application. Ensure you have Node.js (LTS version recommended) and npm (or yarn) installed.
  • A TypeScript Project: You'll need a TypeScript project or be willing to set one up.

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:

  • @stoplight/spectral-core: The heart of Spectral, containing the linting engine.
  • @stoplight/spectral-functions: Provides a collection of built-in functions that your rules can use (e.g., alphabetical, defined, pattern, truthy, xor).
  • @stoplight/spectral-rulesets: Offers pre-defined rulesets like spectral:oas (for OpenAPI) and spectral:asyncapi.
  • typescript: The TypeScript compiler.
  • ts-node: Allows you to run TypeScript files directly without pre-compiling, useful for development.

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:

  • description: A human-readable explanation of the rule.
  • message: The error or warning message to display if the rule is violated. Can include placeholders like {{error}}, {{path}}, {{value}}.
  • severity: Defines the impact of the rule violation. Can be error, warn, info, or hint.
  • given: A JSONPath expression (or an array of them) specifying which parts of the document the rule applies to.
  • then: Defines the action to take on the targeted values. This usually involves applying one or more functions.
  • function: The name of the built-in or custom function to execute.
  • functionOptions: Options to pass to the function.
  • formats: An array specifying which document formats this rule applies to (e.g., oas3, oas2, asyncapi2).

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:

  • extends: Inherit rules from other rulesets (e.g., built-in Spectral rulesets or shared organizational rulesets).
  • rules: An object containing your custom rule definitions.
  • functionsDir: Specifies a directory where custom JavaScript function files are located.
  • functions: An array of custom functions (less common when using functionsDir or programmatic setup).

Functions:

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

  • truthy: Checks if a value is truthy.
  • falsy: Checks if a value is falsy.
  • defined: Checks if a property is defined.
  • undefined: Checks if a property is undefined.
  • pattern: Checks if a string matches a regex.
  • alphabetical: Checks if array elements or object keys are in alphabetical order.
  • length: Checks the length of a string or array.
  • schema: Validates a value against a JSON Schema.

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:

  • We import IFunction and IFunctionResult from @stoplight/spectral-core for type safety. IFunction<T = unknown, O = unknown> takes two generic arguments: T for the type of targetVal (the value being linted) and O for the type of options passed to the function.
  • targetVal: The actual value from the API document that the given JSONPath points to. We've typed it as string.
  • options: An object containing options passed from the rule definition (e.g., { "maxLength": 70 }). We created an interface TitleCaseLengthOptions for these options.
  • context: Provides contextual information about the linting process, including:
  • path: An array of path segments leading to the targetVal.
  • document: The entire parsed document.
  • rule: The current rule being processed.
  • The function returns an array of IFunctionResult objects if there are violations, or void/undefined/empty array if there are no issues. Each IFunctionResult must have a message and optionally a path (if different from context.path).
  • Our logic checks for a simple title case format and the maximum length.

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:

  • No Compilation Step for Ruleset Definition: The ruleset itself is defined in TypeScript. Your custom functions still need to be imported (either as TS if ts-node is used, or compiled JS if running pure Node).
  • Dynamic Rules: You can programmatically construct or modify rulesets based on runtime conditions.
  • Direct Type Usage: You directly use your imported TypeScript functions within the ruleset definition, enhancing type safety and IDE integration.

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.

  • Modular Rulesets: Break down large rulesets into smaller, focused files. You can use the extends property to combine them. JavaScript
// .spectral.js (main ruleset)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... other rules or overrides
};
  • Shared Rulesets: Publish rulesets as npm packages to share them across different projects or teams. The extends property can then reference these packages.
  • functionsDir for Simplicity: If you have many custom JS functions (compiled from TS), functionsDir in a .spectral.yaml ruleset can be simpler than listing them all or using a fully programmatic setup. Just ensure your compiled JS files export the functions correctly.

Integrating Spectral into Your Workflow

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

  • CI/CD Pipelines: Integrate spectral lint commands into your GitHub Actions, GitLab CI, Jenkins, or other CI/CD pipelines. Fail the build if critical errors are detected. YAML
# Example GitHub Action step
- name: Lint API Specification
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error
  • Git Hooks: Use tools like Husky to run Spectral on pre-commit or pre-push hooks, catching issues before they even reach the repository.
  • IDE Integration: Look for Spectral extensions for your IDE (e.g., VS Code) to get real-time feedback as you write your API specifications.

Best Practices for Spectral and TypeScript Custom Rules

  • Clear Descriptions and Messages: Write meaningful description and message properties for your rules. Messages should guide the user on how to fix the issue.
  • Appropriate Severity Levels: Use error for critical violations, warn for important suggestions, info for informational checks, and hint for minor suggestions.
  • Precise given Paths: Make your JSONPath expressions as specific as possible to target only the intended nodes. This improves performance and reduces false positives.
  • Idempotent Functions: Custom functions should be pure where possible – given the same input, they should produce the same output without side effects.
  • Test Your Custom Rules: Write unit tests for your custom TypeScript functions to ensure they behave as expected with various inputs.
  • Performance Considerations: Be mindful of complex computations or numerous schema function calls in performance-sensitive rules, especially for very large documents. Test the performance of your rulesets.
  • Keep TypeScript Types Updated: Ensure your custom functions correctly type the targetVal and options they expect. This is crucial for maintainability.

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.

Top 10 AI Doc Generators & API Documentation Makers for 2025Viewpoint

Top 10 AI Doc Generators & API Documentation Makers for 2025

Whether you're documenting internal systems, creating user guides, or publishing detailed API references, the right tool can make all the difference. Let's dive into the best options available today.

Emmanuel Mumba

May 19, 2025

A Complete Introduction to the JSON:API SpecificationViewpoint

A Complete Introduction to the JSON:API Specification

This comprehensive guide offers a deep dive into the JSON:API specification, exploring its core concepts, structure, and powerful features.

Stefania Boiko

May 19, 2025

Top 10 Best API Newsletters & Podcasts You Cannot Miss in 2025Viewpoint

Top 10 Best API Newsletters & Podcasts You Cannot Miss in 2025

This article presents a curated selection of top API-centric newsletters and podcasts that are indispensable for anyone serious about mastering the API domain in 2025.

Mikael Svenson

May 19, 2025