Linting

Linting Your API

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

A Zuplo gateway is mostly configuration: OpenAPI files in config/ describe your routes, and config/policies.json defines the policies those routes run. A typo in that configuration — a policy name that doesn't exist, a required option left out, a route with no authentication — doesn't fail a type check. It shows up later, as a failed build or a route that behaves differently than you expect.

zuplo lint catches those problems on your machine or in CI, before you deploy. It reads the same files the gateway reads and reports each problem with the file, line, and column where it occurs.

TerminalCode
npx zuplo lint

What the linter checks

The linter runs three kinds of checks against every *.oas.json file in your project's config/ directory and against config/policies.json:

  • OpenAPI quality — every operation has an operationId, a description, and tags; path parameters are declared; no two paths are ambiguous.
  • Zuplo route configuration — every policy a route references exists in policies.json, x-zuplo-route blocks contain only valid properties, and routes have a label. The stricter presets also require authentication and rate limiting on every route, and zuplo:strict requires a CORS setting.
  • Policy configuration — policies.json has the right structure, and each built-in policy's handler.options match that policy's schema, so a missing requestsAllowed on a rate limit policy or a misspelled enum value shows up as a warning before you deploy.

Optionally, the linter also compares your OpenAPI files against a Git branch and reports breaking changes, such as a removed endpoint.

The full list of checks, with an example of each, is in the rules reference.

Run the linter

Run the command from the root of your Zuplo project — the directory that contains config/:

TerminalCode
npx zuplo lint

The linter prints each result grouped by file, followed by a summary:

Code
Zuplo API Lint Config: zuplo:recommended | failOn: error | source: default config/routes.oas.json 6:14 warn Operations should have descriptions: `description` must be set operation-description $.paths['/todos'].get.description 6:14 error Every operation must have an operationId: `operationId` must be set operation-operationId $.paths['/todos'].get.operationId 6:14 warn Operations should have tags: `tags` must be set operation-tags $.paths['/todos'].get.tags 6:14 warn Route get /todos should have a label in x-zuplo-route zuplo/require-route-label paths./todos.get.x-zuplo-route.label 15:56 error Policy "rate-limit-inbound" is referenced in route but not defined in policies.json zuplo/no-undefined-policies paths./todos.get.x-zuplo-route.policies.inbound[1] 19:5 error parameter named `id` must be defined as part of the path `/todos/{id}` definition, or in the `DELETE` operation(s) path-params $.paths['/todos/{id}'] 20:17 error Every operation must have an operationId: `operationId` must be set operation-operationId $.paths['/todos/{id}'].delete.operationId 20:17 warn Route delete /todos/{id} should have a label in x-zuplo-route zuplo/require-route-label paths./todos/{id}.delete.x-zuplo-route.label 31:36 warn Route delete /todos/{id} has an empty inbound policies array zuplo/no-empty-inbound-policies paths./todos/{id}.delete.x-zuplo-route.policies.inbound ✖ 4 errors, 5 warnings remaining

Each line shows the line:column of the problem, its severity, a message, the ID of the rule that reported it, and the location inside the document. Use the rule ID to look the rule up in the rules reference or to change its severity in your configuration.

The header shows which presets ran, the severity that fails the run, and where the configuration came from: default when there is no configuration file, file (auto-detected) when the linter found a zuplo.lint.jsonc, or file (--config) when you passed one.

When nothing is left to report, the run ends with No violations found. and exits with 0.

To lint a project in another directory, pass --dir:

TerminalCode
npx zuplo lint --dir ./apps/gateway

Errors, warnings, and the exit code

Every rule reports at a severity of error or warn. By default, the command fails only when it finds at least one error, so warnings point out improvements without blocking you.

Exit codeMeaning
0No results at or above the failure threshold.
1The linter found results at or above the failure threshold.
2The linter couldn't run: the directory isn't a Zuplo project, or the configuration file is invalid.

Change the threshold with --fail-on:

TerminalCode
# Fail on warnings as well as errors npx zuplo lint --fail-on warn # Report everything but never fail npx zuplo lint --fail-on none

To set the threshold for everyone on the project, set failOn in the configuration file instead. The --fail-on flag overrides it.

Fix problems automatically

Some problems have exactly one correct fix. Run --fix to apply those fixes to your OpenAPI files, then lint the result:

TerminalCode
npx zuplo lint --fix
Code
Fixed config/routes.oas.json ✔ Added operationId "getTodos" to GET /todos operation-operationId ✔ Added operationId "deleteTodosById" to DELETE /todos/{id} operation-operationId ✔ Removed empty inbound policies array from DELETE /todos/{id} zuplo/no-empty-inbound-policies

The results printed after the fixes are the problems that still need your attention.

--fix repairs two rules:

RuleFix
operation-operationIdAdds an operationId built from the method and path. DELETE /todos/{id} becomes deleteTodosById.
zuplo/no-empty-inbound-policiesRemoves an empty policies.inbound array, and the policies object if nothing else is left in it.

Everything else — descriptions, tags, labels, missing policies, path parameters, authentication — needs a decision only you can make, so the linter reports it and leaves the file alone.

Automatic fixes are safe to run repeatedly:

  • Formatting is preserved. The linter edits only what it fixes (plus a trailing comma when it removes a property), so the change shows up as a small diff rather than a reformatted file. Files with nothing to fix aren't written. If an OpenAPI file isn't valid JSON, the linter changes nothing and exits with 2.
  • Generated operationId values are unique across the whole project, not only within one file. If the name is already taken, the linter adds a number: getTodos2.
  • Your configuration is respected. A rule that's off for a route, through rules, overrides, or ignorePaths, isn't fixed on that route.
  • Running it twice changes nothing the second time.

Commit or stash your changes before running --fix, so the fixes show up as their own diff and are easy to review.

Output formats

The default text output is for reading in a terminal. Two other formats are for tools:

FormatUse it for
textReading results in a terminal. Set NO_COLOR=1 to turn off colors.
jsonScripts and custom CI steps. Includes a summary, every result, breaking changes, and fixes.
sarifGitHub code scanning and other tools that read SARIF 2.1.0.
TerminalCode
npx zuplo lint --format json > lint-results.json npx zuplo lint --format sarif > lint-results.sarif

The json output has this shape:

JSONCode
{ "status": "failed", "passed": false, "summary": { "errors": 1, "warnings": 0, "breakingChanges": 0, "fixed": 0 }, "config": { "extends": ["zuplo:recommended"], "failOn": "error", "source": "default" }, "violations": [ { "ruleId": "zuplo/no-undefined-policies", "severity": "error", "message": "Policy \"rate-limit-inbound\" is referenced in route but not defined in policies.json", "file": "config/routes.oas.json", "path": "paths./todos.get.x-zuplo-route.policies.inbound[1]", "line": 15, "column": 56 } ], "breakingChanges": [], "fixes": [] }

See Lint in CI/CD for workflows that use each format.

Configure the linter

With no configuration, the linter runs the zuplo:recommended preset and fails on errors. To choose a stricter preset, change or scope rules, add OpenAPI and OWASP rulesets, write your own rules, or enable breaking change detection, add a zuplo.lint.jsonc file to the root of your project:

JSONzuplo.lint.jsonc
{ "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json", "version": 2, "extends": ["zuplo:strict"], "rules": { "operation-tags": "off", }, "overrides": [ { "paths": ["/health"], "rules": { "zuplo/require-policy": "off" } }, ], }

See Lint configuration for every option, and run npx zuplo lint --list-rules to see every rule you can use.

Next steps

Last modified on