A route path is one of the smallest things in a Kong configuration, and one of the easiest to get wrong. A missing slash, an empty string, a regex without its ~ prefix. In my experience these slip through code review because they look harmless, and the first thing that notices is the gateway at deploy time, or worse, your users after it.
This post shows a small deck file lint ruleset that catches those mistakes in CI, before any gateway is involved. Every case was run against a real Kong.
Versions used: Kong Gateway 3.16.0.1 · decK 1.62.1 · source references from Kong 3.9.4 (open source)
Why this matters
Most teams that manage Kong declaratively already run deck file validate in their pipeline. It’s fast, it needs no gateway, and it feels like a safety net. For route paths, it mostly isn’t one.
I wrote 13 small decK files, one per kind of path mistake, and ran each through three checks: deck file validate (offline), the lint ruleset from this post, and Kong itself (kong config parse inside a Kong 3.16 container, which runs Kong’s own schema validation). The result:
| Case | deck file validate | Lint ruleset | Kong 3.16 |
|---|---|---|---|
paths: ["/api/v1/orders"] | pass | clean | accepted |
paths: ["~/orders/\d+$"] (regex) | pass | clean | accepted |
No paths, only hosts | pass | error (policy) | accepted |
| No paths, hosts, methods or headers | pass | error | rejected |
paths: [] | pass | error | rejected |
paths: [""] | pass | error | rejected |
paths: [" "] | pass | error | rejected |
paths: "/api/v1/orders" (string, not array) | fail | error | rejected |
paths: ["api/v1/orders"] (no leading slash) | pass | error | rejected |
paths: ["/api/v1 orders"] (space) | pass | error (policy) | accepted |
paths: ["/api//orders"] (empty segment) | pass | error | rejected |
paths: ["/"] (catch-all) | pass | warning (policy) | accepted |
paths: ["/orders/\d+$"] (regex without ~) | pass | warning (policy) | accepted |
Two things stand out. Offline validation caught one case out of thirteen, the wrong data type. Six configs that Kong rejects passed it, so the pipeline goes green and the deploy fails. And four configs that Kong happily accepts are still worth stopping, because they don’t do what the author meant.
deck file validate isn’t broken; its documentation says it validates against static schemas and “may catch fewer errors” than online validation (decK docs). Path rules live in Kong’s own schema, which the offline check doesn’t fully apply.
What Kong actually requires of a route path
Before writing rules, it helps to know which ones mirror Kong and which ones are your choice.
Kong enforces these. In Kong’s schema, a route path must start with / for a plain prefix or ~/ for a regex, and it must not contain empty segments (//). The error messages are the ones you’ll see at deploy time: should start with: / (fixed path) or ~/ (regex path) and must not have empty segments (typedefs.lua).
Paths are optional. A route doesn’t need paths at all. For HTTP it needs at least one of methods, hosts, headers or paths, and for HTTPS snis also counts (routes_subschemas.lua). That’s why a route with an empty paths array and nothing else fails with must set one of 'methods', 'hosts', 'headers', 'paths', 'snis'.
Regex needs a ~. Since Kong 3.0 there is no guessing: a path is a regex only if it starts with ~, and everything else is plain text (breaking changes, 3.0). So /orders/\d+$ is accepted, but Kong treats it as a plain-text prefix, so it only matches requests that literally start with those characters.
Spaces are allowed. I expected Kong to reject /api/v1 orders. It didn’t. That’s why the whitespace rule below is marked as policy.
The ruleset
deck file lint runs rules written in the vacuum format: a given JSONPath selects values, a function checks them, and a severity decides whether the run fails (decK docs). Save this as route-paths.ruleset.yaml:
# route-paths.ruleset.yaml
# The first two rules mirror checks Kong itself enforces, so CI fails before Kong is involved.
# The rules marked "Policy" are choices: Kong accepts these, we do not.
rules:
route-path-must-start-with-slash:
description: "A route path must start with / (prefix) or ~/ (regex)"
given: $.services[*].routes[*].paths[*]
severity: error
then:
function: pattern
functionOptions:
match: "^~?/"
route-path-no-empty-segments:
description: "A route path must not contain empty segments (//)"
given: $.services[*].routes[*].paths[*]
severity: error
then:
function: pattern
functionOptions:
notMatch: "//"
route-path-no-whitespace:
description: "Policy: no spaces or other whitespace in a route path"
given: $.services[*].routes[*].paths[*]
severity: error
then:
function: pattern
functionOptions:
notMatch: "\\s"
route-must-have-paths:
description: "Policy: every route declares paths (Kong also allows hosts, methods or headers alone)"
given: $.services[*].routes[*]
severity: error
then:
field: paths
function: truthy
route-paths-must-be-non-empty-array:
description: "Policy: every route declares paths as a non-empty array"
given: $.services[*].routes[*]
severity: error
then:
field: paths
function: schema
functionOptions:
schema:
type: array
minItems: 1
items:
type: string
minLength: 1
route-no-catch-all:
description: "Policy: no catch-all / route (it swallows unmatched traffic)"
given: $.services[*].routes[*].paths[*]
severity: warn
then:
function: pattern
functionOptions:
notMatch: "^/$"
route-regex-needs-tilde:
description: "Policy: a path that looks like a regex must start with ~ (Kong 3.x treats it as plain text otherwise)"
given: $.services[*].routes[*].paths[*]
severity: warn
then:
function: pattern
functionOptions:
notMatch: "^/.*(\\\\[dws]|\\[.+\\]|\\.\\*|\\$$)"
A few choices worth explaining:
- Two rules for “has paths”. A
schemacheck on a field doesn’t fire when the field is missing entirely, soroute-must-have-pathsusestruthyto catch the missing case and the schema rule catches the empty and wrong-type cases. I found this out the hard way: with only the schema rule, routes without apathskey passed. - Warnings for the catch-all and the regex heuristic. A
/route is sometimes intentional (a default backend), and the regex check is a heuristic. Warnings show up in the output without failing the build. - Drop the policy rules you don’t want. If your team uses host-only routes, delete
route-must-have-pathsandroute-paths-must-be-non-empty-array. The first two rules are the ones that mirror Kong.
Run it
Lint a decK file with the ruleset:
deck file lint -s kong.yaml route-paths.ruleset.yaml
For a file with a path missing its leading slash, the output is:
Linting Violations: 1
Failures: 1
[error][7:17] A route path must start with / (prefix) or ~/ (regex): `api/v1/orders` does not match the expression `^~?/`
The [7:17] is the line and column in your file, which makes the finding easy to fix in a pull request. The command exits non-zero when there are errors, so it fails a CI step on its own. Add --format json if you want to post the results somewhere.
Wire it into CI
In GitHub Actions, the Kong/setup-deck action installs a pinned decK version. Run lint next to your existing validate step so you keep both checks:
# .github/workflows/kong-config.yaml
name: kong-config
on:
pull_request:
paths: ["kong/**"]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Kong/setup-deck@v1
with:
deck-version: "1.62.1"
- name: Validate (offline)
run: deck file validate kong/kong.yaml
- name: Lint route paths
run: deck file lint -s kong/kong.yaml kong/route-paths.ruleset.yaml
Keep the ruleset in the same repository as the configuration, so a rule change goes through the same review as a route change.
In production
- Lint is not a substitute for online validation. Lint knows only the rules you wrote. Before you apply to a real environment,
deck gateway validatechecks the file against a live Admin API without changing anything, and catches what neither offline check knows about (decK docs). A good pipeline runs lint on every pull request and online validation against a non-production gateway before the merge. - Cover every place routes live. The
givenpaths above only look at routes nested under services. If your files also declare top-levelroutes:, duplicate the path rules with$.routes[*].paths[*]. - Version the ruleset like code. When you upgrade decK or Kong, re-run your known-bad examples against the new versions. That’s how I found the space case: my original rule was based on an assumption, not a test.
- The expressions router is different. If your routes use Kong’s expressions router, matching lives in an
expressionfield, notpaths, and these rules won’t see it.
Trade-offs and when not to
- Rules drift from Kong. The two “Kong enforces this” rules copy Kong’s schema. If Kong changes, your rules won’t follow on their own, which is why the known-bad examples are worth keeping.
- Policy rules need an owner. A team that legitimately routes on hosts only will find
route-must-have-pathsannoying. Agree the policy first, then encode it; don’t let the linter become the place where design decisions are made by accident. - For small, rarely changed configs, online validation against a test gateway may be all you need. Linting pays off when many people change routes, or when routes are generated.
Takeaways
deck file validatecaught 1 of 13 broken or risky route paths in the examples above. Don’t treat it as your only check.- Kong requires paths to start with
/or~/and to have no empty segments. Everything else in this ruleset is policy, so label it that way. - A route without paths is valid Kong. Whether it’s valid for your team is a decision, not a fact.
- Keep known-bad example files next to the ruleset, and run them again on every decK or Kong upgrade.
References
- deck file lint, Kong docs
- deck file validate and deck gateway validate, Kong docs
- Routes entity, Kong docs
- Breaking changes in Kong Gateway 3.0: regex paths need
~, Kong docs - Route path rules in Kong’s schema and route match requirements, Kong 3.9.4 source
- vacuum rule functions, used by
deck file lint - Kong/setup-deck, GitHub Action