Skip to content

REST API Maintaining the Catalogue

Ed Mozley edited this page Jul 4, 2026 · 1 revision

πŸ› οΈ Maintaining the API catalogue (spec.json)

This is the developer guide for changing the REST API β€” adding an endpoint, a whole module, a parameter, or deprecating something β€” and keeping the interactive docs and the OpenAPI document correct with the least effort.


The one thing to understand first

There is one file you edit: api/v1/spec.json. Everything else is generated from it.

File What it is Do you edit it?
api/v1/spec.json FreeITSM's own catalogue: every endpoint's summary, description, parameters, examples and errors Yes β€” this is the source
api/v1/lib/routes.php The route table the front controller dispatches Yes, when adding/removing a route
api/v1/lib/openapi_schemas.php Typed response schemas No β€” a tool maintains it
/api/v1/openapi.json The standard OpenAPI document No β€” generated from the above
system/api/docs.php The interactive docs page No β€” it reads spec.json

Both the interactive docs page and the OpenAPI document read spec.json, so a single edit updates both. openapi.json is generated β€” never hand-edited (see the OpenAPI page for that distinction).


Anatomy of a spec.json entry

spec.json is { "spec": [ …sections… ], "extras": { …examples & errors… } }.

A section groups endpoints under a heading:

{ "section": "Tickets", "items": [ …endpoint objects… ] }

An endpoint object in items:

{
  "m": "POST",                      // GET | POST | PATCH | DELETE
  "p": "/tickets/{id}/notes",       // path; {id} placeholders become path params
  "perm": "ticket_notes.create",    // the permission it needs ("resource.action", or "none")
  "s": "Add a note to a ticket",    // one-line summary
  "d": "Longer description of what it does and any quirks.",
  "params": [                       // path + query parameters (not body fields)
    { "name": "id", "in": "path", "desc": "Ticket id", "req": true },
    { "name": "internal", "in": "query", "desc": "true = internal note", "req": false }
  ],
  "body": { "text": "Looks resolved on my end." }   // example request body (POST/PATCH only)
}

The matching extras entry (keyed "METHOD /path") holds the worked examples and endpoint-specific errors:

"POST /tickets/{id}/notes": {
  "examples": [
    { "title": "Simplest", "note": "Just the text.", "path": { "id": 42 }, "body": { "text": "…" } }
  ],
  "errors": [
    { "code": 409, "when": "The ticket is in the recycle bin." }
  ]
}

Everything else β€” the security scheme, the {data, meta} envelope, the generic error responses (401/403/404/422/429), the success code, and the typed response schema β€” is added by the generator. You only supply what's specific to the endpoint.


Scenario: add a parameter to an existing endpoint

  1. Add it to that endpoint's params array in spec.json:
    { "name": "include_archived", "in": "query", "desc": "true = include archived", "req": false }
  2. (Optional) add an example under the endpoint's extras that uses it, so beginners see it in context.
  3. Run the checks (below).

That's it β€” the docs page shows the new field and the OpenAPI document lists the query parameter, from the one edit.


Scenario: add a new endpoint to an existing module

  1. Add the route to api/v1/lib/routes.php and write the handler in the module's api/v1/resources/*.php. Add a new permission to api/v1/lib/permissions.php if the action is new.
  2. Scaffold the catalogue entry β€” this reads the route table and prints a ready-to-paste stub for anything not yet documented:
    php api/v1/dev/openapi_stub.php
    
    Paste the stub into the right section's items in spec.json and fill in s, d, params, an example body, and any extras.
  3. Type its response from a live call (see The verification loop).
  4. Run the checks.

The drift-guard (part of the checks) fails until the route has a catalogue entry, so you cannot forget step 2.


Scenario: add a whole new module

Same as adding an endpoint, at module scale, and it slots into the existing module recipe:

  1. Build the resource file, routes and permissions as for any module.
  2. php api/v1/dev/openapi_stub.php prints stubs for every new route β€” paste them under a new section in spec.json (give the section a heading, e.g. "section": "Widgets"), and fill them in. Add a wiki usage page for the module if it warrants one.
  3. Type the module's response schemas: php api/v1/dev/openapi_fix.php <read_key> (re-run to 0 patches).
  4. Run the checks. The new section's endpoints appear in the docs nav and the OpenAPI tags automatically.

Scenario: deprecate an endpoint or parameter

The API keeps working, but the specification tells consumers to stop using it. Set "deprecated": true on the entry in spec.json:

{ "m": "GET", "p": "/old-thing", "perm": "…", "s": "…", "deprecated": true, "params": [] }

or on a single parameter:

{ "name": "legacy_id", "in": "query", "desc": "Use object_id instead.", "req": false, "deprecated": true }

The generated OpenAPI operation/parameter is marked deprecated: true, which Swagger UI, Redoc and Postman render struck through with a warning. Explain the replacement in the d (description) so consumers know what to move to. When you're ready to actually remove it, delete the route from routes.php and the entry from spec.json (the drift-guard confirms the two stay in step).

Note

Removing a field from a response is a breaking change even if the endpoint stays β€” drop it from the typed schema in openapi_schemas.php (re-running openapi_fix.php against an install that no longer returns it will not re-add it) and call it out in the endpoint's d.


The verification loop

After any change, from the app root (needs only PHP):

# 1. bring typed response schemas in line with live data (needs a read key)
php api/v1/dev/openapi_fix.php <read_key>        # re-run until "0 patches"

# 2. confirm every schema matches a real response
php api/v1/dev/openapi_verify.php <read_key>     # aim for "0 failed"

# 3. confirm the invariants (drift, refs, operationIds, responses, shape, nullable, catalogue)
php api/v1/lib/openapi_check.php                 # must print "PASS"

# 4. confirm conformance to the official OpenAPI 3.0 meta-schema
curl -s http://localhost/freeitsm-app/api/v1/openapi.json > /tmp/o.json
php api/v1/dev/jsonschema4.php /tmp/o.json api/v1/dev/oas-3.0-schema.json

Create the read key under System β†’ API with the permissions you're touching, and delete it afterwards. api/v1/dev/README.md has the same workflow next to the tools.


What the checks protect you from

openapi_check.php (step 3) is the safety net β€” it exits non-zero on any of:

  • an endpoint in routes.php with no spec.json entry, or vice versa (drift);
  • a spec.json entry that's malformed β€” bad method, missing summary, a parameter with no in (catalogue);
  • an extras key that doesn't match a real endpoint;
  • a response schema referencing a component that doesn't exist (refs);
  • duplicate operation ids, an operation with no responses, or the subtler OpenAPI traps (empty-array-as-object, nullable without type).

So the honest summary: edit spec.json, run the loop, and the checks tell you if anything is inconsistent β€” you don't have to hold the whole system in your head.

See also: REST API: OpenAPI specification Β· how it's kept correct Β· the interactive docs page.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally