Toolchain documentation

apidef

The front half. A spec goes in, a model of entities and operations comes out, with a record of why every classification was made.

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. Checked against the source repository, which is the authority where it and this page disagree.

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.

Concepts#

The ideas you need to hold to use it, and the ones that cost time when nobody told you.

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#

Commands and API calls are as the component's own documentation gives them. Every example marked with a source is copied from that public repository, so it can be checked rather than trusted.

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
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
"/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
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
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#

The lookup tables. The first-party documentation below goes deeper on every row.

CLI options

NameWhat it is
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, -wWatch mode.
--debug, -g <level>Log level. Defaults to info.
--help, -h / --version, -vPrint help or version, then exit.

The model

NameWhat it is
apimodel.main.kit.infoWhat the spec said about itself.
apimodel.main.kit.entityThe entities: fields, op, id, relations.
apimodel.main.kit.flowOrdered operation sequences with assertions, one per entity.
op.<name>.points[]Each concrete path and method that yields this operation.
point.orig / point.segmentsThe source path as written, and that path resolved into { lit } and { var }.
point.args.params[]What the caller supplies: name, orig, reqd, type.
point.selectexist lists identifiers that must already exist; $action marks an action point.
point.transformRequest and response envelope handling, for APIs that wrap the entity.

The guide

NameWhat it is
guide.entity.<name>.pathThe 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.paramParameter renames, such as moon_id to id.
path.actionPresent when the path is an action on the entity rather than an entity of its own.
guide.metricsTotals and a PATH MISMATCH guard confirming every source method was classified.

The rest of the toolchain#

  • sdkgen Turns the model into SDKs.
  • create-sdkgen Scaffolds a project.
  • docgen Generates documentation targets.
  • apigen Not yet published.

The pipeline, the components, and the two worked examples

Read the generated code#

The toolchain is MIT and open, and the catalog holds 600+ generated SDKs readable without installing anything.

Voxgig SDK GeneratorTalk to Voxgig

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.