# apidef

> Turns a spec into a model. The front half. It reads an OpenAPI 3 or Swagger 2 spec and produces an internal model of entities, operations, fields and flows, inferring what the spec leaves implicit and recording why it decided each thing. Part of the Voxgig SDK toolchain. Checked 8 September 2026.

## What it is

apidef's output is not cleaned-up OpenAPI. It is a different data structure, designed around one question: what does a client SDK need to know?

A spec describes paths. An SDK author thinks in resources. `/pets` and `/pets/{id}` are one resource scattered across two entries, and no part of the spec says so. apidef inverts that: the entity is the primary unit, and the paths that produced it are folded inside it.

Getting there means inferring what OpenAPI leaves implicit: which paths form one resource, which methods are create, read, update or delete, how parameters map to identifiers, which responses wrap the entity in an envelope. Those are heuristics, and heuristics are wrong sometimes, so every decision is recorded with a `why_` trace you can read.

The model lives at `apimodel.main.kit` and has three top-level collections: `info`, `entity` and `flow`. It is rendered to `jsonic` files on disk, and that on-disk form is the contract with sdkgen.

Those files are editable, and the next run merges into your edits rather than clobbering them. Hold the distinction all the same: the model is a document you own and the toolchain refreshes around, while the generated targets are output that a resync of a vendored target can revert. Edit the model; read the targets.

## Facts

- Package: @voxgig/apidef (https://www.npmjs.com/package/@voxgig/apidef)
- Source: voxgig/apidef (https://github.com/voxgig/apidef)
- Licence: MIT
- Runtime: Node.js 24 or later
- Reads: OpenAPI 3, Swagger 2
- Writes: entities, operations, fields, flows, and a guide

## Concepts

### Entities, not paths

An entity carries `fields` (the data shape, from the schemas), `op` (load, list, create, update, remove, patch), `id` (which field identifies an instance) and `relations` (the entities it nests under). This is the structure an SDK mirrors directly, which is why the generated surface reads `client.Planet().list()` rather than a method named after a URL.

### An operation has points, not a path

One logical operation can be reachable through more than one path. In solardemo, `planet.create` is produced both by `POST /api/planet` and by the action path `POST /api/planet/{id}/terraform`. So an operation is not a path plus a method: it is a list of `points`, each one concrete path and method that yields that operation. Keeping the list plural is what lets the model carry actions, alternate routes and collection-versus-item variants without losing information.

### Segments are resolved, not parsed later

Each point records `orig`, the path as the spec wrote it, and `segments`, that path already resolved into `{ lit }` and `{ var }` parts. Downstream code never has to parse braces out of a string, which is one whole class of bug that does not happen.

### Args carry both names

Each point's `args.params` lists what the caller must supply, and every arg records both its canonical `name` (`id`) and its `orig` wire name (`planet_id`), plus whether it is required and its inferred type. That dual naming is exactly what lets a generated SDK present a clean `id` argument while still building the correct URL.

### Types become validator tokens

Field types are normalised to validator tokens, `$STRING`, `$NUMBER` and `$BOOLEAN`, rather than left as raw OpenAPI types, so validation downstream is uniform across every target language. A field's requiredness can differ per operation, required on `create` and optional on `update`; when it does, the difference is recorded under the field's `op` map rather than flattened away.

### Flows are executable expectations

Beyond the static shape, apidef emits flows: ordered sequences that exercise an entity, typically create, list and expect present, update, load and expect the update, remove, list and expect absent. A flow is a machine-readable integration test of the generated SDK, which is where a generated SDK's live suite comes from.

### The guide is the escape hatch

The classification stage writes `base-guide.aontu`: which paths belong to which entity, how each method was classified, and a `why_path` and `why_op` trace for each. It is meant to be edited when a heuristic guesses wrong, and it is merged rather than overwritten on the next run. For an API that does not follow REST conventions, this is the file you correct instead of fighting the spec.

## Examples

### Read what the classifier decided (from solardemo)

The guide for solardemo shows `moon` assembled from two paths: the collection path yields `create` and `list`, the item path yields `load`, `update` and `remove`, and the item id is canonicalised from `moon_id` to `id`. Nothing here was in the spec as a statement; all of it was inferred and recorded.

`base-guide.aontu, abridged`

```jsonic
entity: moon: {
  name: moon
  path: {
    "/api/planet/{planet_id}/moon": {
      op: {
        create: { method: POST }
        list:   { method: GET }
      }
    }
    "/api/planet/{planet_id}/moon/{moon_id}": {
      rename: { param: { moon_id: id } }   # the item id is canonicalized
      op: {
        load:   { method: GET }
        update: { method: PUT }
        remove: { method: DELETE }
      }
    }
  }
}
```

### An action is not a separate entity (from solardemo)

solardemo's planet has a `terraform` path. A naive reading makes that a resource called terraform. apidef marks it as an action on `planet` instead, which is why the generated SDK offers it on the planet entity rather than inventing a type nobody would recognise.

`base-guide.aontu, the planet entity`

```jsonic
"/api/planet/{planet_id}/terraform": {
  action: { terraform: {} }        # an action on planet, not an entity
  rename: { param: { planet_id: id } }
  op: { create: { method: POST } }
}
```

If this is wrong for your API, edit it here. The next run merges your correction rather than discarding it.

### The entity model the generator reads (from elementdemo)

This is what an entity looks like once classification is done. Fields carry validator tokens and a `short` description where the spec supplied one; `id` names the identifying field; each operation lists its points. elementdemo's `element` has fourteen fields, so this is abridged to the shape.

`.sdk/model/entity/element.aon, abridged`

```aontu
main: kit: entity: element: {

  fields: [
    {
      name: "id"
      req: true
      short: "Element identifier, the lowercase symbol."
      type: "`$STRING`"
    }
    {
      name: "mass"
      req: true
      short: "Standard atomic weight in daltons."
      type: "`$NUMBER`"
    }
    {
      name: "group"
      req: false
      short: "Periodic table column, 1 to 18, absent for the f-block."
      type: "`$INTEGER`"
    }
    # ... eleven more
  ]

  id: { field: "id", name: "id" }
  name: "element"

  op: {
    load: {
      name: "load"
      points: [
        {
          method: "GET"
          orig: "/element/{element_id}"
          segments: [ { lit: "element" }, { var: "id" } ]
          rename: { param: { element_id: "id" } }
          select: { exist: [ "id" ] }
          transform: { req: "`reqdata`", res: "`body`" }
        }
      ]
    }
    # ... list, create, update, remove
  }
}
```

`select.exist` says which identifiers must already exist for this point to address anything. That becomes the SDK's routing and its precondition checks.

### Drive it from code

The library interface is the primary way to run apidef and is what the test suite and the rest of the toolchain use. The CLI is a thin wrapper over it, useful for a one-off run against an existing project layout.

`Node.js`

```javascript
const { ApiDef } = require('@voxgig/apidef')

const build = await ApiDef.makeBuild({
  folder: '/proj/model',
  outprefix: 'solardemo-',
})

const result = await build(
  { name: 'solardemo', def: 'solardemo.yml' },  // model: name + spec file
  { spec: { base: '/proj/model' } },            // build: where the spec is read from
  {},
)

const entities = result.apimodel.main.kit.entity
// entities.planet.op     -> { load, list, create, update, remove }
// entities.planet.fields -> [ { name: 'id', type: '`$STRING`', req: true }, ... ]
// result.guide           -> the classification, with its why_ traces
```

## Reference

### CLI options

| | |
| --- | --- |
| `voxgig-apidef <name>` | The first positional argument is the project name. |
| `--folder, -f <dir>` | Project folder root. Defaults to the `<name>` argument. |
| `--def, -d <spec>` | Path to the spec file. Validated to exist. |
| `--watch, -w` | Watch mode. |
| `--debug, -g <level>` | Log level. Defaults to `info`. |
| `--help, -h / --version, -v` | Print help or version, then exit. |

### The model

| | |
| --- | --- |
| `apimodel.main.kit.info` | What the spec said about itself. |
| `apimodel.main.kit.entity` | The entities: `fields`, `op`, `id`, `relations`. |
| `apimodel.main.kit.flow` | Ordered operation sequences with assertions, one per entity. |
| `op.<name>.points[]` | Each concrete path and method that yields this operation. |
| `point.orig / point.segments` | The source path as written, and that path resolved into `{ lit }` and `{ var }`. |
| `point.args.params[]` | What the caller supplies: `name`, `orig`, `reqd`, `type`. |
| `point.select` | `exist` lists identifiers that must already exist; `$action` marks an action point. |
| `point.transform` | Request and response envelope handling, for APIs that wrap the entity. |

### The guide

| | |
| --- | --- |
| `guide.entity.<name>.path` | The source paths assigned to this entity. |
| `path.why_path[]` | The trace of why this path joined this entity. |
| `path.op.<name>.why_op[]` | The trace of the CRUD classification. |
| `path.rename.param` | Parameter renames, such as `moon_id` to `id`. |
| `path.action` | Present when the path is an action on the entity rather than an entity of its own. |
| `guide.metrics` | Totals and a `PATH MISMATCH` guard confirming every source method was classified. |

## First-party documentation

- [apidef documentation index](https://github.com/voxgig/apidef/tree/main/docs)
- [Tutorial: getting started](https://github.com/voxgig/apidef/blob/main/docs/tutorial/getting-started.md)
- [Explanation: the internal model, and why it exists](https://github.com/voxgig/apidef/blob/main/docs/explanation/the-internal-model.md)
- [Explanation: how path classification works](https://github.com/voxgig/apidef/blob/main/docs/explanation/classification-heuristics.md)
- [Reference: the internal API model](https://github.com/voxgig/apidef/blob/main/docs/reference/model.md)
- [Reference: the guide model](https://github.com/voxgig/apidef/blob/main/docs/reference/guide.md)
- [Reference: the command-line tool](https://github.com/voxgig/apidef/blob/main/docs/reference/cli.md)
- [How-to: customize entity naming](https://github.com/voxgig/apidef/blob/main/docs/how-to/customize-entity-naming.md)
- [AGENTS.md: how the description becomes the model](https://github.com/voxgig/apidef/blob/main/AGENTS.md)

## The rest of the toolchain

- [The toolchain documentation index](https://voxgig.com/sdk/docs): the pipeline, the components, and the two worked examples.
- [sdkgen](https://voxgig.com/sdk/docs/sdkgen): Turns the model into SDKs.
- [create-sdkgen](https://voxgig.com/sdk/docs/create-sdkgen): Scaffolds a project.
- [docgen](https://voxgig.com/sdk/docs/docgen): Generates documentation targets.
- apigen: Not yet published.
- [Voxgig SDK Generator](https://voxgig.com/sdk)
