# Linting Your API

> **Scope of this page**
>
> This page documents the Zuplo **gateway** runtime (API, AI, and MCP gateways), not the Developer Portal.
>
> It applies to every Zuplo hosting option: Managed Edge, Managed Dedicated, and Self-Hosted. See [Hosting options](https://zuplo.com/docs/articles/hosting-options).

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.

```bash
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](./api-lint-breaking-changes.mdx), such as a removed
endpoint.

The full list of checks, with an example of each, is in the
[rules reference](./api-lint-rules.mdx).

## Run the linter

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

```bash
npx zuplo lint
```

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

```txt
  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](./api-lint-rules.mdx) or to
change its severity in your [configuration](./api-lint-configuration.mdx#rules).

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`:

```bash
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 code | Meaning                                                                                             |
| --------- | --------------------------------------------------------------------------------------------------- |
| `0`       | No results at or above the failure threshold.                                                       |
| `1`       | The linter found results at or above the failure threshold.                                         |
| `2`       | The linter couldn't run: the directory isn't a Zuplo project, or the configuration file is invalid. |

Change the threshold with `--fail-on`:

```bash
# 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`](./api-lint-configuration.mdx#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:

```bash
npx zuplo lint --fix
```

```txt
  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:

| Rule                              | Fix                                                                                                   |
| --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `operation-operationId`           | Adds an `operationId` built from the method and path. `DELETE /todos/{id}` becomes `deleteTodosById`. |
| `zuplo/no-empty-inbound-policies` | Removes 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`](./api-lint-configuration.mdx#overrides), or
  [`ignorePaths`](./api-lint-configuration.mdx#ignorepaths), isn't fixed on that
  route.
- **Running it twice changes nothing the second time.**

:::tip

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:

| Format  | Use it for                                                                                  |
| ------- | ------------------------------------------------------------------------------------------- |
| `text`  | Reading results in a terminal. Set `NO_COLOR=1` to turn off colors.                         |
| `json`  | Scripts and custom CI steps. Includes a summary, every result, breaking changes, and fixes. |
| `sarif` | GitHub code scanning and other tools that read SARIF 2.1.0.                                 |

```bash
npx zuplo lint --format json > lint-results.json
npx zuplo lint --format sarif > lint-results.sarif
```

The `json` output has this shape:

```json
{
  "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](./api-lint-ci-cd.mdx) 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:

```jsonc title="zuplo.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](./api-lint-configuration.mdx) for every option, and run
`npx zuplo lint --list-rules` to see every rule you can use.

## Next steps

- [Lint configuration](./api-lint-configuration.mdx) — presets, rule severities,
  required policies, and breaking change detection.
- [Lint rules reference](./api-lint-rules.mdx) — what each rule checks and how
  to fix it.
- [Custom lint rules](./api-lint-custom-rules.mdx) — write your own rules and
  share them across projects.
- [Breaking change detection](./api-lint-breaking-changes.mdx) — compare your
  API with a Git branch and catch changes that break consumers.
- [Lint in CI/CD](./api-lint-ci-cd.mdx) — fail pull requests on lint errors and
  show results in GitHub code scanning.
- [`zuplo lint` CLI reference](../cli/lint.mdx) — every flag the command
  accepts.
