Two engineers on the same team ship two endpoints in the same week. One returns created_at, the other returns createdAt. One paginates with ?page=2, the other with ?offset=20. One puts errors in a top-level error object, the other inlines a message string. Both pass code review because reviewers are reading logic, not naming. Six months later, your API surface reads like it was written by five different companies, and every client integration needs a special case.
An OpenAPI linter catches that drift before it ships. It reads your OpenAPI document, runs it against a set of rules, and fails the build when a rule is broken. Think ESLint for JavaScript or RuboCop for Ruby, but pointed at your API contract instead of your application code.
Typical rules include:
- Operations must have descriptions.
- Schemas must include examples.
- Property names must follow one case convention.
- Every response must declare a media type.
- Error responses must follow one shared shape.
💡 If you design and test your APIs in Apidog, you also get consistency checks at design time plus a CLI that gates the rest of your pipeline, so the linter is not the only thing standing between a sloppy spec and production.
What an OpenAPI linter actually checks
A linter operates on the spec file, not on a running server. Point it at openapi.yaml, and it walks every path, operation, parameter, schema, and response, applying rules one at a time.
Most rules fall into four categories.
Validity
Is this a legal OpenAPI document?
Examples:
- Does every
$refresolve? - Are required keywords present?
- Are schemas structurally valid?
- Are paths and operations defined correctly?
This overlaps with plain schema validation, and most linters do it before anything else.
Completeness
Does the spec contain enough information to generate useful docs, SDKs, and client integrations?
Examples:
- Every operation has an
operationId. - Every operation has a
summaryanddescription. - Every parameter explains itself.
- Every schema includes an
example. - Every response documents its payload.
These are the rules humans forget most often.
Consistency
This is where linting pays off.
Examples:
- Property names use one case convention.
- Path segments follow a consistent format.
- Collection paths use plural nouns.
- Error responses share one shape.
- Every
2xxresponse declaresapplication/json. - Status codes are used consistently.
None of these issues are always bugs in isolation. Together, they determine whether your API feels designed or assembled.
House style
These are your team-specific rules.
Examples:
- Every endpoint must be tagged.
-
DELETEmust return204. - Internal-only fields must use a prefix.
- Public APIs must include examples.
- Deprecated operations must include migration notes.
House-style rules are where a linter becomes part of your engineering process instead of just a generic validator.
A rule also has a severity: error, warning, info, or hint.
Use severity to adopt linting gradually:
- Start new rules as warnings.
- Fix the worst violations.
- Promote important rules to errors.
- Fail CI only on errors.
That lets you add linting to an existing API without blocking every pull request on day one.
For more background on enforcing consistency across teams, see how top companies ensure API design consistency.
The main OpenAPI linter options
Here are the OpenAPI linting tools worth knowing and where each one fits.
Spectral
Spectral, from Stoplight, is the de facto standard OpenAPI linter. It is an open-source CLI and library that lints OpenAPI 2.0, OpenAPI 3.x, AsyncAPI, and general JSON/YAML documents.
It ships with a built-in spectral:oas ruleset for common OpenAPI checks. Its biggest strength is custom rules. You define rules with JSONPath-style given selectors and then functions in YAML.
Common built-in functions include:
truthypatterncasinglengthenumeration
You can also write custom JavaScript functions when declarative rules are not enough.
Spectral is a good default if you want:
- A widely recognized tool.
- A large rule ecosystem.
- VS Code/editor integration.
- A CLI that runs anywhere Node runs.
- Strong custom-rule support.
The tradeoff is that nontrivial rules require learning JSONPath and, eventually, Spectral’s function API. For a deeper implementation guide, see building custom Spectral rules with TypeScript.
A minimal .spectral.yaml:
extends: ["spectral:oas"]
rules:
operation-operationId: error
operation-description: warn
property-casing:
description: Properties must be camelCase
given: $.components.schemas..properties[*]~
severity: error
then:
function: casing
functionOptions:
type: camel
Run it:
npx @stoplight/spectral-cli lint openapi.yaml
To fail only on error-level rules:
npx @stoplight/spectral-cli lint openapi.yaml --fail-severity=error
Redocly CLI
Redocly’s CLI bundles linting with bundling and documentation preview.
Its linter reads a redocly.yaml config, ships with built-in rules, and supports configurable rulesets plus custom plugins written in JavaScript.
Redocly CLI is a good fit if:
- You already use Redocly for documentation.
- You want linting, bundling, and preview in one toolchain.
- Your linting rules mostly support documentation quality.
Run it:
npx @redocly/cli lint openapi.yaml
The tradeoff: compared with Spectral, the rule ecosystem is smaller and the custom-rule path is less broadly documented.
Vacuum
Vacuum is a newer OpenAPI linter written in Go and built for speed.
It is compatible with Spectral rulesets, so you can point it at an existing .spectral.yaml and run many of the same checks much faster on large specs.
Vacuum is useful when:
- You have large OpenAPI files.
- You lint many specs in a monorepo.
- CI runtime matters.
- You want a single binary with no Node runtime.
For small specs, the speed gain may not matter. The ecosystem and editor tooling are also younger than Spectral’s, so Vacuum is often most attractive as a CI accelerator.
Swagger and openapi-spec-validator
Swagger Editor, swagger-cli, and openapi-spec-validator validate whether a document is legal OpenAPI.
That is useful, but it is not the same as linting.
Validation catches issues like:
- Invalid OpenAPI structure.
- Broken references.
- Missing required OpenAPI fields.
It does not catch design inconsistency.
A validator will pass a spec where one schema uses created_at, another uses createdAt, and another uses creationDate, because the OpenAPI specification does not forbid that.
Validation is the floor. Linting is how you enforce design quality.
If you are comparing Swagger-family tools with broader API design platforms, see Swagger alternatives that also test your API.
Design-time checks in Apidog
The tools above run after you already have an OpenAPI file. Another place to catch inconsistency is before the file exists: while you are designing the endpoint.
Apidog is a design-first API platform. You build endpoints and data schemas in a visual editor, and your project stays internally consistent as you work.
Reusable data schemas help prevent the same model from being redefined differently per endpoint. Shared response components do the same for error shapes.
Apidog is not a drop-in replacement for a Spectral ruleset. If you have committed .spectral.yaml rules, keep running them.
What Apidog changes is how much your linter finds in the first place. When the design layer encourages reuse, the linter goes from reporting a wall of violations to catching the occasional mistake.
Because Apidog imports and exports standard OpenAPI 3.x, the file you hand to Spectral or Vacuum in CI is the same artifact. The design layer and linter stack together instead of competing.
A linter setup you can run today
A practical linting setup runs checks in three places:
- Editor
- Pre-commit hook
- CI
Then, after linting, run API tests against the implementation.
Layer 1: the editor
Install the Spectral VS Code extension and add .spectral.yaml to your repo root.
The extension picks up the rules file automatically and underlines violations as you edit the spec.
That gives developers feedback before they commit.
Recommended repo layout:
.
├── openapi.yaml
├── .spectral.yaml
└── package.json
Example .spectral.yaml:
extends: ["spectral:oas"]
rules:
operation-operationId: error
operation-description: warn
operation-summary: warn
oas3-api-servers: warn
property-casing:
description: Schema properties must use camelCase
given: $.components.schemas..properties[*]~
severity: error
then:
function: casing
functionOptions:
type: camel
Layer 2: pre-commit
Add a local hook so broken specs do not reach the remote.
In package.json:
{
"scripts": {
"lint:api": "spectral lint openapi.yaml --fail-severity=error"
},
"devDependencies": {
"@stoplight/spectral-cli": "^6.0.0"
}
}
Install dependencies:
npm install
Create a Git hook:
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
npm run lint:api || {
echo "OpenAPI lint failed. Fix the spec before committing."
exit 1
}
EOF
chmod +x .git/hooks/pre-commit
Or use Husky if your team already manages hooks that way.
The important flag is:
--fail-severity=error
That makes the linter exit non-zero only for error-level rules. Warnings still print, but they do not block the commit.
Layer 3: CI
CI is the gate that matters because teammates cannot bypass it with --no-verify.
A minimal GitHub Actions workflow:
name: API lint
on: [pull_request]
jobs:
spectral:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx @stoplight/spectral-cli lint openapi.yaml --fail-severity=error
When an error-level rule fails:
- The job exits non-zero.
- The pull request gets a red check.
- The merge is blocked.
- The author fixes the OpenAPI file.
No separate dashboard is required. The PR status is the enforcement mechanism.
Layer 4: test the API the spec describes
A linter proves the spec is valid and consistent. It does not prove the running API matches the spec.
That gap is where contract drift hides. You can have a perfectly linted OpenAPI document that describes behavior the server no longer implements.
Close that gap by running API tests against the live service in the same pipeline.
This is where the Apidog CLI fits next to your linter. It is an npm package, apidog-cli, and it runs your Apidog test scenarios from the command line.
Install it:
npm install -g apidog-cli
Run a test scenario:
apidog run --access-token $APIDOG_ACCESS_TOKEN -t 605067 -e 1629989 -r cli,junit
The command exits non-zero when a test fails, which means CI can block the merge just like it does for a lint failure.
Useful options:
-
--access-token: authenticates the CLI. -
-t: selects the test scenario or test target. -
-e: selects the environment, such as staging or production. -
-r cli,junit: prints CLI output and emits JUnit XML.
The JUnit reporter is useful because most CI systems can parse it into a pass/fail test tree.
A combined GitHub Actions flow can look like this:
name: API quality gate
on: [pull_request]
jobs:
api-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Lint OpenAPI spec
run: npx @stoplight/spectral-cli lint openapi.yaml --fail-severity=error
- name: Install Apidog CLI
run: npm install -g apidog-cli
- name: Run API tests
env:
APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
run: apidog run --access-token $APIDOG_ACCESS_TOKEN -t 605067 -e 1629989 -r cli,junit
Lint first, test second.
- The lint step is fast and catches design problems.
- The test step is slower and catches behavior problems.
For the full pipeline pattern, including reporters and exit-code handling, see running the Apidog CLI in your CI/CD pipeline. If you use GitHub Actions, the Apidog CLI in GitHub Actions has a copy-paste workflow.
Choosing and adopting a ruleset without the pain
Picking a linter is usually easy. Adopting it on an existing API is harder.
The first run on a mature spec may return hundreds or thousands of violations. Do not respond by turning the linter off.
Use a staged rollout instead.
Step 1: Start from a built-in ruleset
For Spectral, start with:
extends: ["spectral:oas"]
Then run:
npx @stoplight/spectral-cli lint openapi.yaml
Do not make every rule an error immediately.
Step 2: Fix validity issues first
Validity issues are real bugs.
Prioritize:
- Broken
$refvalues. - Invalid schemas.
- Missing required OpenAPI fields.
- Incorrect response definitions.
- Invalid parameter declarations.
These should become errors early because they affect tooling, documentation generation, and client generation.
Step 3: Pick two or three consistency rules
Choose the rules that matter most to client developers.
Good first candidates:
- Property casing.
- Required
operationId. - Required response media types.
- Shared error response shape.
- Consistent pagination parameters.
Example:
extends: ["spectral:oas"]
rules:
operation-operationId: error
property-casing:
description: Schema properties must use camelCase
given: $.components.schemas..properties[*]~
severity: error
then:
function: casing
functionOptions:
type: camel
Keep less urgent rules as warnings.
Step 4: Promote warnings over time
Each sprint, promote one or two warnings to errors.
Example adoption plan:
Week 1:
- Valid OpenAPI: error
- operationId: error
- descriptions: warn
- examples: warn
Week 3:
- property casing: error
- response media type: error
- examples: warn
Week 6:
- error response shape: error
- pagination style: error
Week 10:
- remaining documentation rules: error
This lets the team keep shipping while the spec improves.
Step 5: Write custom rules only when needed
Custom rules are useful, but every custom rule becomes something your team must maintain and explain.
A rule earns its place when a violation has already caused pain.
Good reasons to add a custom rule:
- Client integrations repeatedly break because of inconsistent naming.
- Error responses are difficult to handle.
- Pagination differs across endpoints.
- Internal fields leaked into public schemas.
- SDK generation depends on a specific convention.
Weak reasons:
- Personal preference.
- Bikeshedding.
- A convention nobody consumes.
- A rule that creates more exceptions than value.
For guidance on which rules are worth enforcing, see API design best practices.
If you design APIs in a language other than raw YAML, linting still applies. TypeSpec compiles down to OpenAPI, and you lint the emitted document the same way. The linter does not care how the file was authored. It only cares what the OpenAPI document says.
Where the linter fits in the bigger design loop
A linter is one control in a design-first workflow, not the whole workflow.
A complete API quality loop looks like this:
- Design the contract.
- Lint the OpenAPI document.
- Mock the API so clients can build early.
- Test the implementation against the contract.
- Publish documentation from the same source.
Skip any step and the others lose value.
- A linted spec nobody mocks still blocks frontend work.
- A mocked spec nobody tests can drift from production.
- Published docs generated from an inconsistent spec create bad client experiences.
The reason to put design first is the same reason linting works: problems are cheaper to fix earlier.
Changing a property name in a design tool is one edit. Changing it after three teams have shipped against the old name is a migration.
The linter enforces consistency on the file. A design-first process enforces consistency on the decision before the file exists.
For the broader workflow tradeoffs, see API-first vs API design-first vs code-first and contract-first API design tools.
Apidog covers that loop in one place: design with reusable schemas, mock instantly, test with the CLI in CI, and export clean OpenAPI for whichever linter you standardize on.
The linter still has a job. There is just less for it to catch.



Top comments (0)