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=, the other with ?offset=. Neither is wrong on its own. Together they make your API feel like it was assembled by strangers, and every client that consumes it pays the tax. The OpenAPI file validates fine. It parses, it renders in Swagger UI, it generates an SDK. It is just inconsistent, and a plain validator has nothing to say about that.
That is the exact gap a linter fills. A validator answers “is this spec legal OpenAPI?” A linter answers “does this spec follow the rules we agreed on?” The most popular open-source tool for the second question is Spectral, Stoplight’s linter for JSON and YAML documents. It ships with a built-in OpenAPI ruleset, lets you write your own rules, and runs from the terminal or your editor. If you want a free, scriptable way to enforce an API style guide, Spectral is the obvious first stop, and this guide shows you how to use it properly.
It also shows you the trade. Spectral is a ruleset you assemble and maintain. For teams who would rather get consistency checks, mock servers, and runnable tests from one place without hand-writing YAML rules, Apidog folds that work into the design surface itself. We will credit Spectral fully first, then show where the all-in-one path saves you the upkeep.
What Spectral actually does
Spectral is a generic linter. You point it at a structured document, give it a set of rules, and it reports every place the document breaks a rule, with a line number and a severity. It is not specific to OpenAPI; it understands OpenAPI, AsyncAPI, and Arazzo out of the box, and you can lint any JSON or YAML file with custom rules.

The reason it matters for API work is the built-in spectral:oas ruleset. That ruleset encodes a long list of OpenAPI conventions: operations should have an operationId, the info object should carry a description and contact, tags should be defined before they are used, parameters should not duplicate each other. Run it against a real-world spec and you will almost always get a list of warnings on the first try. None of them break a parser. All of them make the spec harder to live with.
This is a different job from structural validation. A tool like swagger-cli or Redocly answers whether the document conforms to the OpenAPI schema. Spectral answers whether the document follows your house style on top of that. You want both, and the two checks compose cleanly in a pipeline. We walk through the validation half in the guide on how to validate OpenAPI specs; this article is about the style-and-consistency half.
Installing Spectral and running your first lint
Spectral ships as an npm package. The CLI is @stoplight/spectral-cli. Install it globally:
npm install -g @stoplight/spectral-cli
Node.js is the only system dependency, so any machine or CI image with Node already installed can run it. If you would rather not install it globally, npx @stoplight/spectral-cli ... works on ephemeral build runners.
Spectral needs a ruleset to know what to check. The convention is a file called .spectral.yaml in your working directory. The smallest useful one extends the built-in OpenAPI rules:
# .spectral.yaml
extends: ["spectral:oas"]
Now lint a spec. With a .spectral.yaml in the current directory, Spectral picks it up automatically:
spectral lint openapi.yaml
Or point at a ruleset explicitly:
spectral lint openapi.yaml --ruleset .spectral.yaml
The output is readable on purpose. Each finding shows the line and column, the severity, the rule that fired, and a human message:
openapi.yaml
3:6 warning info-contact Info object should contain `contact` object.
5:10 error info-description OpenAPI object info `description` must be present.
✖ 2 problems (1 error, 1 warning, 0 infos, 0 hints)
That first run against an existing spec is the moment most teams realize how much drift has crept in. The rules were never enforced, so nobody followed them.
Writing your own rules
The built-in ruleset is a starting point, not the destination. The real value of Spectral is encoding your team’s conventions as rules a machine checks on every change. A rule has four moving parts: what to look at (given, a JSONPath expression), what to check (then, a function), how loud to be (severity), and what to say when it fails (message).
Here is a rule that enforces kebab-case paths, a common house convention:
# .spectral.yaml
extends: ["spectral:oas"]
rules:
paths-kebab-case:
description: Paths should be kebab-case.
message: "{{property}} should be kebab-case (lower-case, hyphen-separated)"
severity: warn
given: $.paths[*]~
then:
function: pattern
functionOptions:
match: "^(\\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$"
The given selects every path key. The then runs the built-in pattern function against a regular expression. Anything that fails the pattern gets reported as a warning with your message. You can ban integer IDs in favor of UUIDs, require an error response on every POST, forbid version numbers in server URLs, or require that every operation carries a description. Spectral ships several core functions (truthy, pattern, schema, length, enumeration, and more) so most conventions need no code at all.
When a rule needs logic a function option cannot express, Spectral lets you write rules in JavaScript or TypeScript and import custom functions. That is where the tool gets powerful and where the maintenance starts. If you want to go that deep, we have a full walkthrough on building custom Spectral rules with TypeScript.
Severity, and making the build fail
Every Spectral rule has a severity: error, warn, info, or hint. By default the CLI only exits with a non-zero code when it finds an error. Warnings print but do not fail the run. That is fine while you are cleaning up a legacy spec and do not want a thousand warnings to block every merge.
Once your spec is clean, tighten the gate. The --fail-severity flag controls the threshold:
spectral lint openapi.yaml --fail-severity=warn
Now a warning also returns exit code 1, which is what a CI step reads to mark itself failed. This is the mechanism that turns a linter into an actual quality gate: the pipeline blocks the merge the moment the spec drifts from the style guide. You can also override individual rule severities in your ruleset, bumping a rule you care about from warn up to error or silencing one that does not fit your team by setting it to off.
Running Spectral in CI
A linter that runs only when someone remembers is not a gate. The point is to run it on every push, on a clean machine, with the same ruleset for everyone. Spectral makes this short. Here is a GitHub Actions job that lints the spec on any pull request that touches it:
name: Lint OpenAPI
on:
pull_request:
paths:
- "openapi.yaml"
jobs:
spectral:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g @stoplight/spectral-cli
- run: spectral lint openapi.yaml --fail-severity=warn
For richer reporting, Spectral can emit JUnit XML, which most CI dashboards parse into a pass/fail tree:
spectral lint openapi.yaml -f junit -o results.xml
Wire that artifact into your dashboard and every contributor sees which rule failed and where, without reading raw logs. If you want the broader picture of layering structural checks, linting, and breaking-change detection together, the OpenAPI-in-CI patterns generalize past any single tool. Treating the spec as code is the mindset that makes all of this stick.
Where Spectral asks a lot of you
Spectral is good at what it does. The honest catch is that it does one thing, and the rest of the spec lifecycle is your problem to stitch together. A few realities show up once a team adopts it past the demo.
You own the ruleset. The built-in spectral:oas rules are generic. Your real style guide lives in custom rules you write, review, version, and keep current as conventions evolve. That ruleset becomes a small codebase with its own maintenance burden, and JSONPath plus custom functions is a skill not everyone on the team has.
It lints the document, not the API. Spectral reads the file. It cannot tell you whether the running service actually returns what the spec promises. A spec can pass every lint rule and still describe an endpoint the implementation drifted away from months ago. Closing that gap means sending real requests and asserting on responses, which is a different tool entirely.
It is one piece of a longer chain. After linting you still need mocks for frontend teams, a docs site, and an automated test suite. Each is a separate tool to install, configure, and keep in sync with the spec. The linter does not know about any of them, so the spec gets re-parsed and re-interpreted at every stage.
None of this is a knock on Spectral. It is a focused linter and it is honest about its scope. But “focused” means the integration work lands on you.
The easier way: consistency built into the design surface
Here is the other path. Instead of treating consistency as a lint step bolted on after the spec is written, Apidog treats it as part of writing the spec.
Apidog is an all-in-one API platform: you design the schema, debug requests, build test scenarios, mock endpoints, and publish docs in one workspace. Because the design happens inside the tool, the consistency checks happen as you type. The visual designer surfaces structural problems the moment they appear, the way a compiler underlines a syntax error, so you fix them before they ever reach a commit. You are not running a separate linter after the fact; the editor is the linter.
The bigger difference is everything downstream. The same contract that you design becomes your mock server, your interactive docs, and your test scenarios, with no re-parsing and no second tool to keep in sync. When you want those checks in a pipeline, the Apidog CLI runs your test scenarios headlessly from the terminal and exits non-zero on failure, exactly the gate behavior you wanted from a linter, except it tests the running API against the contract instead of only reading the file. Install it with one npm command and point it at a scenario:
npm install -g apidog-cli
apidog run --access-token $APIDOG_ACCESS_TOKEN -t <scenarioId> -e <environmentId> -r cli
That fills the gap Spectral leaves open. Spectral confirms the document follows your style. The Apidog CLI confirms the implementation still matches the document. For the full flag reference, run apidog run --help or read the complete CLI guide.
So the trade is real and worth stating plainly. Spectral gives you a free, scriptable, vendor-neutral linter you assemble and maintain. Apidog gives you consistency, mocking, docs, and runnable tests from one source of truth, with far less to wire together. If a portable lint step in an existing toolchain is all you need, Spectral is a fine answer. If you want the whole lifecycle to hold without becoming a tooling project of its own, the integrated path costs you less over time.
Spectral vs Apidog at a glance
| Capability | Spectral | Apidog |
|---|---|---|
| OpenAPI style linting | Yes, via spectral:oas + custom rules |
Yes, surfaced live in the designer |
| Custom rules | Yes, YAML or JS/TS, you maintain them | Conventions enforced by the editor, no rule code |
| Structural validation | With Redocly or a validator alongside | Built in at design time |
| Mock server | No | Yes |
| Auto-generated docs | No | Yes |
| Runnable API tests | No | Yes, via the Apidog CLI |
| CI gate | spectral lint --fail-severity=warn |
apidog run non-zero exit |
| Cost | Free, open source | Free tier, paid plans |
Use the table as a decision aid, not a scoreboard. The right choice is the one that matches how much of the lifecycle you want one tool to own.
Frequently asked questions
Is Spectral free? Yes. Spectral is open source under the Apache 2.0 license, maintained by Stoplight. The CLI, the built-in OpenAPI ruleset, and custom-rule authoring are all free to use.
Does Spectral validate that my spec is legal OpenAPI? Partly. The built-in rules catch many structural issues, but Spectral is a linter, not a dedicated schema validator. Pair it with a validator for full structural coverage. The guide on validating OpenAPI specs covers that side, and the best OpenAPI validator tools compares the options.
Can Spectral test my running API? No. Spectral reads the spec file only. To check that the live API matches the contract, you need a runner that sends real requests and asserts on responses, such as the Apidog CLI.
How do I make a Spectral warning fail my CI build? Run spectral lint openapi.yaml --fail-severity=warn. By default only error severity fails the build; --fail-severity=warn makes warnings return a non-zero exit code too.
What is the difference between Spectral and Apidog? Spectral is a focused open-source linter you configure and maintain. Apidog is an all-in-one platform where design, consistency checks, mocking, docs, and testing live together, so you assemble less and keep less in sync. See Apidog vs Swagger for a related comparison of the design-tool landscape.
Wrapping up
Spectral solves a real problem the simple validators ignore: keeping an OpenAPI spec consistent with the conventions your team agreed on. Install @stoplight/spectral-cli, extend spectral:oas, add a few custom rules, and gate your pipeline with --fail-severity=warn. For many teams that is enough, and it costs nothing.
The cost shows up later, in the rules you maintain and the rest of the lifecycle you stitch around the linter. If you would rather get consistency, mocks, docs, and runnable tests from one source of truth, download Apidog and build your spec where the checks are already part of the surface. Either way, the goal is the same: a spec your whole team can trust, enforced by a machine instead of a hope.



