Como Usar Spectral com TypeScript: Guia Completo

Audrey Lopez

Audrey Lopez

19 maio 2025

Como Usar Spectral com TypeScript: Guia Completo

Manter consistência, qualidade e aderência aos padrões de design é fundamental para equipes que utilizam APIs. Especificações de API como OpenAPI e AsyncAPI fornecem um modelo, mas garantir que esses modelos sejam seguidos corretamente em diversos serviços e equipes pode ser um desafio assustador. É aqui que entram as ferramentas de linting de API, e o Spectral se destaca como uma opção open-source flexível e poderosa. Quando combinado com TypeScript, o Spectral capacita os desenvolvedores a criar regras personalizadas robustas e type-safe, elevando a governança de API a um novo nível.

Este tutorial irá guiá-lo pelo processo de aproveitar o Spectral com TypeScript, desde a configuração inicial até a criação de lógica de validação personalizada sofisticada. Exploraremos como o TypeScript aprimora o desenvolvimento de regras do Spectral, levando a soluções de linting de API mais fáceis de manter e mais confiáveis.

💡
Quer uma ótima ferramenta de Teste de API que gera documentação de API bonita?

Quer uma plataforma integrada e completa para sua Equipe de Desenvolvedores trabalhar em conjunto com produtividade máxima?

Apidog atende a todas as suas demandas e substitui o Postman por um preço muito mais acessível!
button

Entendendo o Spectral: O Guardião das Suas Especificações de API

Antes de mergulharmos na integração com TypeScript, vamos estabelecer o que é o Spectral e por que ele é uma ferramenta valiosa em seu kit de ferramentas de desenvolvimento de API.

Spectral é um linter JSON/YAML open-source com foco principal em formatos de descrição de API como OpenAPI (v2 e v3) e AsyncAPI. Seu propósito é ajudar a impor diretrizes de design de API, detectar erros comuns e garantir consistência em todo o seu cenário de API. Pense nele como ESLint ou TSLint, mas especificamente para seus contratos de API.

Principais Benefícios de Usar o Spectral:

O Spectral opera com base em rulesets (conjuntos de regras). Um ruleset é uma coleção de regras, onde cada regra segmenta partes específicas do seu documento de API (usando expressões JSONPath) e aplica lógica de validação. O Spectral vem com rulesets integrados (por exemplo, spectral:oas para padrões OpenAPI), mas seu verdadeiro poder reside na capacidade de definir rulesets personalizados.

Por Que TypeScript Para Regras Personalizadas do Spectral?

Embora os rulesets do Spectral possam ser definidos em YAML ou JavaScript (arquivos .js), usar TypeScript para desenvolver funções personalizadas oferece vantagens significativas:

Ao escrever suas funções personalizadas do Spectral em TypeScript, você traz o mesmo rigor e benefícios de ferramentas para o seu código de governança de API que você faz para o seu código de aplicação.

Configurando Seu Ambiente Spectral e TypeScript

Vamos colocar a mão na massa e configurar as ferramentas necessárias.

Pré-requisitos:

Passos de Instalação:

Primeiro, você precisará do CLI do Spectral para executar operações de linting e testar suas regras. Geralmente é útil instalá-lo globalmente ou usar npx.Bash

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

Para desenvolver regras personalizadas programaticamente e usar as bibliotecas centrais do Spectral dentro de um projeto TypeScript, instale os pacotes Spectral necessários: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

Vamos detalhar esses pacotes:

Configurar TypeScript:

Crie um arquivo tsconfig.json na raiz do seu projeto se você ainda não tiver um. Uma configuração básica pode parecer assim: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"]
}

Ajuste outDir e rootDir de acordo com a estrutura do seu projeto. Assumiremos que suas funções TypeScript personalizadas residirão no diretório src.

Conceitos Centrais do Spectral: Regras, Rulesets e Funções

Antes de escrever funções TypeScript, vamos solidificar nosso entendimento dos principais componentes do Spectral.

Regras:

Uma regra define uma verificação específica a ser realizada. As propriedades principais de uma regra incluem:

Rulesets:

Um ruleset é um arquivo YAML ou JavaScript (por exemplo, .spectral.yaml, .spectral.js ou .spectral.ts quando compilado) que agrupa regras. Ele também pode:

Funções:

Funções são as unidades de lógica central que realizam a validação real. O Spectral fornece muitas funções integradas como:

O verdadeiro poder surge quando estas não são suficientes e você precisa escrever suas próprias funções personalizadas – é aqui que o TypeScript brilha.

Criando Sua Primeira Função Spectral Personalizada em TypeScript

Vamos criar uma função personalizada simples. Imagine que queremos impor uma regra de que todos os resumos de operações de API devem estar em Title Case e não exceder 70 caracteres.

Passo 1: Definir a Função Personalizada em TypeScript

Crie um arquivo, digamos src/customFunctions.ts:TypeScript

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

interface TitleCaseLengthOptions {
  maxLength: number;
}

// Função personalizada para verificar se uma string está em Title Case e dentro de um comprimento máximo
export const titleCaseAndLength: IFunction<string, TitleCaseLengthOptions> = (
  targetVal,
  options,
  context
): IFunctionResult[] | void => {
  const results: IFunctionResult[] = [];

  if (typeof targetVal !== 'string') {
    // Não deveria acontecer se o caminho 'given' apontar para uma string, mas é uma boa prática
    return [{ message: `Value at path '${context.path.join('.')}' must be a string.` }];
  }

  // Verificar Title Case (verificação simples: a primeira letra de cada palavra é maiúscula)
  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], // Caminho para o elemento violador
    });
  }

  // Verificar comprimento
  const maxLength = options?.maxLength || 70; // Padrão para 70 se não fornecido
  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;
};

Explicação:

Passo 2: Compilar a Função TypeScript

Se você planeja usar esta função com um ruleset .spectral.yaml ou .spectral.js que aponta para um functionsDir, você precisará compilar seu TypeScript para JavaScript.

Adicione um script de build ao seu package.json:JSON

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

Execute npm run build ou yarn build. Isso compilará src/customFunctions.ts para dist/customFunctions.js (com base em nosso tsconfig.json).

Passo 3: Criar um Arquivo Ruleset

Vamos criar um ruleset, por exemplo, .spectral.js (ou .spectral.ts se você preferir uma configuração totalmente baseada em TypeScript, veja a próxima seção).

Se estiver usando um arquivo ruleset JavaScript referenciando diretamente as funções compiladas:JavaScript

// .spectral.js
const { titleCaseAndLength } = require('./dist/customFunctions'); // Caminho para suas funções JS compiladas

module.exports = {
  extends: [['@stoplight/spectral-rulesets/dist/rulesets/oas', 'recommended']],
  rules: {
    'operation-summary-title-case-length': {
      description: 'Resumos de operações devem estar em Title Case e não exceder 70 caracteres.', // Translate description
      message: '{{error}}', // A mensagem virá da função personalizada
      given: '$.paths[*][*].summary', // Segmenta todos os resumos de operações
      severity: 'warn',
      formats: ["oas3"], // Aplicar apenas a documentos OpenAPI v3
      then: {
        function: titleCaseAndLength, // Referenciar diretamente a função importada
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    // Você pode adicionar mais regras aqui
  },
};

Alternativamente, se estiver usando .spectral.yaml e functionsDir:

Primeiro, certifique-se de que seu diretório dist contenha um index.js que exporte suas funções, ou que seu customFunctions.js as exporte diretamente. Por exemplo, se dist/customFunctions.js tiver exports.titleCaseAndLength = ...;, você poderia fazer:YAML

# .spectral.yaml
extends:
  - ["@stoplight/spectral-rulesets/dist/rulesets/oas", "recommended"]
functionsDir: "./dist" # Diretório contendo funções JS personalizadas compiladas
rules:
  operation-summary-title-case-length:
    description: "Resumos de operações devem estar em Title Case e não exceder 70 caracteres."
    message: "{{error}}"
    given: "$.paths[*][*].summary"
    severity: "warn"
    formats: ["oas3"]
    then:
      function: customFunctions#titleCaseAndLength # Assumindo que as funções são exportadas de customFunctions.js
      functionOptions:
        maxLength: 70

Aqui, customFunctions#titleCaseAndLength diz ao Spectral para procurar por customFunctions.js (ou customFunctions/index.js) no functionsDir e usar a função titleCaseAndLength exportada.

Passo 4: Criar um Documento OpenAPI de Exemplo

Vamos criar um arquivo openapi.yaml simples para testar nossa regra:YAML

# openapi.yaml
openapi: 3.0.0
info:
  title: API de Exemplo
  version: 1.0.0
paths:
  /items:
    get:
      summary: recupera todos os itens da loja # Incorreto: não está em Title Case
      responses:
        '200':
          description: Uma lista de itens.
    post:
      summary: Adiciona Um Novo Item À Coleção Sempre Crescente De Itens Na Loja # Incorreto: muito longo
      responses:
        '201':
          description: Item criado.
  /users:
    get:
      summary: Obter Detalhes do Usuário # Correto
      responses:
        '200':
          description: Detalhes do usuário.

Passo 5: Executar o Spectral

Agora, execute o CLI do Spectral contra seu documento OpenAPI:Bash

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

Saída Esperada:

Você deverá ver avisos semelhantes a este:

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)

Esta saída confirma que nossa função TypeScript personalizada, compilada para JavaScript, está identificando corretamente as violações.

Uso Programático do Spectral com Rulesets TypeScript

Para cenários mais complexos ou integração mais estreita em aplicações, você pode querer usar o Spectral programaticamente e definir seu ruleset inteiramente em TypeScript. Isso dispensa a necessidade de um arquivo .spectral.yaml ou .spectral.js separado, se desejado, e permite a construção dinâmica de rulesets.

Crie um arquivo TypeScript para seu linter, por exemplo src/linter.ts:TypeScript

import { Spectral, Document } from '@stoplight/spectral-core';
import { oas } from '@stoplight/spectral-rulesets';
import { truthy } from '@stoplight/spectral-functions'; // Exemplo de função integrada
import { titleCaseAndLength } from './customFunctions'; // Sua função TS personalizada
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import * as fs from 'fs/promises';
import * as path from 'path';

// Definir a instância do Spectral
const spectral = new Spectral();

// Carregar rulesets e funções integradas
spectral.setRuleset({
  extends: [[oas, 'recommended']], // Estende as regras OpenAPI recomendadas
  rules: {
    'operation-summary-title-case-length': {
      description: 'Resumos de operações devem estar em Title Case e não exceder 70 caracteres.',
      message: '{{error}}',
      given: '$.paths[*][*].summary',
      severity: 'warn',
      formats: ["oas3"],
      then: {
        function: titleCaseAndLength, // Usar diretamente a função TypeScript
        functionOptions: {
          maxLength: 70,
        },
      },
    },
    'info-contact-defined': { // Exemplo usando uma função integrada
        description: 'O objeto Info deve ter um objeto contact.',
        message: 'Informações de contato da API estão faltando.',
        given: '$.info',
        severity: 'warn',
        formats: ["oas3"],
        then: {
          field: 'contact',
          function: truthy, // Usando uma função integrada
        },
      },
  },
});

// Função para fazer lint em um documento
export async function lintDocument(filePath: string): Promise<ISpectralDiagnostic[]> {
  try {
    const absolutePath = path.resolve(filePath);
    const fileContent = await fs.readFile(absolutePath, 'utf-8');
    
    // Criar um objeto Document do Spectral
    // O segundo argumento (URI) é importante para resolver $refs relativos, se houver
    const document = new Document(fileContent, undefined, absolutePath); 
    
    const results = await spectral.run(document);
    return results;
  } catch (error) {
    console.error('Erro ao fazer lint no documento:', error);
    return [];
  }
}

// Exemplo de uso (por exemplo, em um script ou outro módulo)
async function main() {
  const diagnostics = await lintDocument('openapi.yaml'); // Caminho para sua especificação de API
  if (diagnostics.length > 0) {
    console.log('Problemas de Linting de API Encontrados:');
    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('Nenhum problema de linting de API encontrado. Ótimo trabalho!');
  }
}

// Se você quiser executar este arquivo diretamente com ts-node
if (require.main === module) {
  main().catch(console.error);
}

Para executar isso:Bash

npx ts-node src/linter.ts

Esta abordagem oferece máxima flexibilidade:

Técnicas Avançadas de Funções Personalizadas

Vamos explorar alguns aspectos mais avançados da criação de funções personalizadas.

Funções Personalizadas Assíncronas:

Se sua função personalizada precisar realizar operações assíncronas (por exemplo, buscar um recurso externo para validar, embora use com cautela para desempenho), você pode defini-la como uma função assíncrona.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: `Recurso '${targetVal}' não encontrado em ${options.url}. Status: ${response.status}` }];
    }
  } catch (error: any) {
    return [{ message: `Erro ao buscar recurso '${targetVal}': ${error.message}` }];
  }
};

O Spectral irá corretamente await sua função assíncrona.

Acessando o Documento Completo e Valores Resolvidos:

O objeto context.document lhe dá acesso ao documento parseado inteiro. De forma mais poderosa, context.document.resolved fornece a versão totalmente desreferenciada do documento, o que é essencial ao lidar com ponteiros $ref.TypeScript

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

// Exemplo: Garantir que um schema referenciado tenha uma propriedade específica
export const referencedSchemaHasProperty: IFunction<{$ref: string}, { propertyName: string }> = (
  targetVal, // Este será o objeto como { $ref: '#/components/schemas/MySchema' }
  options,
  context
): IFunctionResult[] | void => {
  if (!targetVal.$ref) return;

  // O método `context.document.resolveAnchor` pode encontrar o valor resolvido para um $ref
  const resolvedValue = context.document.resolveAnchor(targetVal.$ref);

  if (!resolvedValue || !isPlainObject(resolvedValue.value)) {
    return [{ message: `Não foi possível resolver $ref: ${targetVal.$ref}` }];
  }
  
  const schemaProperties = resolvedValue.value.properties as Record<string, unknown> | undefined;

  if (!schemaProperties || schemaProperties[options.propertyName] === undefined) {
    return [{
      message: `O schema referenciado por "${targetVal.$ref}" no caminho '${context.path.join('.')}' deve ter a propriedade "${options.propertyName}".`,
      path: [...context.path, '$ref'] // Apontar para o próprio $ref
    }];
  }
};

Esta função seria usada com um caminho given que segmenta objetos contendo $ref, por exemplo: $.paths[*].*.responses.*.content.*.schema.

Trabalhando com formatos:

Certifique-se de que suas regras personalizadas especifiquem os formatos aos quais se aplicam (por exemplo, oas2, oas3, asyncapi2). Isso impede que as regras sejam executadas em tipos de documentos incompatíveis. Você pode acessar o formato detectado dentro de sua função via context.document.formats.

Organizando e Estendendo Rulesets

À medida que sua coleção de regras personalizadas cresce, a organização se torna fundamental.

// .spectral.js (ruleset principal)
module.exports = {
  extends: ['./rulesets/common-rules.js', './rulesets/security-rules.js'],
  // ... outras regras ou overrides
};

Integrando o Spectral ao Seu Fluxo de Trabalho

O verdadeiro poder do linting de API é realizado quando ele é automatizado.

# Exemplo de passo no GitHub Action
- name: Fazer Lint na Especificação da API
  run: spectral lint ./path/to/your/api-spec.yaml --ruleset ./.spectral.js --fail-severity=error

Melhores Práticas para Spectral e Regras Personalizadas com TypeScript

Conclusão: Eleve Sua Governança de API com Spectral e TypeScript

O Spectral oferece um framework robusto para linting de API, e ao aproveitar o TypeScript para o desenvolvimento de regras personalizadas, você traz segurança de tipo aprimorada, experiência do desenvolvedor e manutenibilidade para sua estratégia de governança de API. Seja para impor melhores práticas OpenAPI, convenções de nomenclatura específicas da empresa ou lógica de negócio complexa dentro de seus designs de API, a combinação do motor de regras flexível do Spectral e do sistema de tipagem forte do TypeScript fornece uma solução poderosa.

Ao integrar o Spectral ao seu ciclo de vida de desenvolvimento, desde o feedback da IDE até pipelines de CI/CD, você pode garantir que suas APIs sejam consistentes, de alta qualidade e adiram aos seus padrões definidos, levando, em última análise, a APIs mais confiáveis e fáceis de consumir. Comece pequeno, itere em seus rulesets e capacite suas equipes com as ferramentas para construir APIs melhores.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs