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

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
operationId
s, 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:
-
Initialize Project:
mkdir spectral-typescript-tutorial cd spectral-typescript-tutorial npm init -y
-
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
-
Configure TypeScript (
tsconfig.json
):
Create atsconfig.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 }
-
Create Source Directory:
mkdir src
-
Create a Sample API Specification:
Let's create a basic OpenAPI 3 specification file to lint. Save this assample-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 bygiven
. 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:
- We import the
oas3
format specifier and some built-in functions (truthy
,pattern
). - We export a default object representing our ruleset.
-
info-description-present
: Checks if the$.info.description
field exists and is truthy. Usesgiven: '$.info'
andthen.field: 'description'
. -
operation-id-kebab-case
: Checks if alloperationId
values match the kebab-case pattern^[a-z][a-z0-9-]*$
. Usesgiven
to target all operation IDs and thepattern
function. Note the{{value}}
placeholder in the message. -
error-response-has-description
: Checks if responses starting with 4 or 5 (like400
,503
) have adescription
. It uses a JSONPath filter?(@property.match(/^[45]/))
to select specific response codes. Note{{property}}
(the response code) and{{path}}
placeholders.
Compiling and Running:
-
Compile TypeScript to JavaScript:
npx tsc
This command reads
tsconfig.json
and compilessrc/spectral.ts
intodist/spectral.js
. -
Run Spectral CLI:
Use the Spectral CLI to lint oursample-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 operationId
s 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.
-
Create a Custom Functions File:
Createsrc/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) andIFunctionResult
(the type for the results it returns). - The function receives
targetVal
(the value selected bygiven
),opts
(options passed from the rule), andcontext
(information about the path and document). - It iterates through the
parameters
array (targetVal
). - For each parameter where
in
isquery
, it checks ifname
matches thesnakeCaseRegex
. - If it doesn't match, it pushes an error object onto the
results
array. Crucially, it specifies the exactpath
to the offending parameter name usingcontext.path
and appending the array index and field name (i
,'name'
).
- We import
-
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] } } };
-
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
orexample
fields against the correspondingschema
.-
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 thesecurity
field isdefined
for that operation. Spectral's resolution logic usually means if it's not defined locally, it looks globally ($.security
). Thedefined
function is a simple check; a more robust check might ensuresecurity
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.
- The
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 anoperationId
. -
operation-id-unique
: Uses the special..
recursive descent JSONPath syntax ($..operationId
) to gather alloperationId
values anywhere in the document into a single list, then applies thenunique
function to ensure there are no duplicates. -
operation-id-min-length
(Optional): Uses thelength
function to check if the string value of theoperationId
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
orpage_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:
-
Local Development:
-
Git Hook: Use tools like
husky
andlint-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.
-
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:
- Checkout the code.
- Set up Node.js.
- Install dependencies (
npm ci
oryarn install
). - Compile the TypeScript ruleset (
npx tsc
). - Run Spectral against your API specification file(s) (
npx spectral lint ...
). - 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!