Skip to content

OpenAPI

Direction of integration. Helix generates OpenAPI 3.0.3 specs from a rest: block. It does not load an external OpenAPI document and generate workflows from it — that direction is not yet supported. If you have an OpenAPI spec from another team, hand-translate the operations into rest: verbs.

Source: pkg/dsl/rest_openapi.go (the generator) and pkg/dsl/rest_reifier.go (the /openapi.json route that serves it).

How it works

When a rest: block is reified, the reifier:

  1. Iterates over every verb in every rest: block.
  2. Builds an openAPISpec with paths, operations, request bodies, responses, parameters, and security schemes derived from the verb definitions (pkg/dsl/rest_openapi.go: GenerateOpenAPI).
  3. Marshals the spec to indented JSON.
  4. Registers a route on the same host:port as the rest: block that serves GET /openapi.json with the marshalled bytes (pkg/dsl/rest_reifier.go: buildOpenAPIRoute).

The spec is generated once at startup from the RestDefinitions in the loaded YAML. There is no live reload of the spec — restart the engine (or use helix run --watch) when you change the rest: block.

What the generator produces

A snippet like:

rest:
  - path: "/api/v1"
    tag: "Users"
    description: "User management API"
    produces: "application/json"
    security:
      - type: bearer
        scheme: "JWT"
    verbs:
      - method: get
        path: "/users"
        target: direct://listUsers
        description: "List all users"
      - method: get
        path: "/users/{id}"
        target: direct://getUser
        description: "Get a single user"
        response-type: "User"
        params:
          - name: id
            type: path
            data-type: integer
            required: true
      - method: post
        path: "/users"
        target: direct://createUser
        request-type: "User"
        response-type: "User"

produces:

{
  "openapi": "3.0.3",
  "info": {
    "title": "Helix REST API",
    "version": "1.0.0"
  },
  "paths": {
    "/api/v1/users": {
      "get": {
        "tags": ["Users"],
        "summary": "List all users",
        "operationId": "get_api_v1_users",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": { "schema": { "type": "object" } }
            }
          }
        },
        "security": [{ "bearerAuth": [] }]
      },
      "post": {
        "tags": ["Users"],
        "operationId": "post_api_v1_users",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/User" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/User" }
              }
            }
          }
        },
        "security": [{ "bearerAuth": [] }]
      }
    },
    "/api/v1/users/{id}": {
      "get": {
        "tags": ["Users"],
        "summary": "Get a single user",
        "operationId": "get_api_v1_users_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "integer" }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/User" }
              }
            }
          }
        },
        "security": [{ "bearerAuth": [] }]
      }
    }
  },
  "tags": [
    { "name": "Users", "description": "User management API" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "JWT" }
    }
  }
}

Field mapping reference

rest: block → top-level spec

Source Mapping
path: Each verb's path: is joined to this to produce the OpenAPI path key.
tag: Operation tags: [<tag>] on every verb; also emitted as a top-level tags: entry.
description: The top-level tag's description.
security: Each entry becomes a components.securitySchemes entry and is attached to every operation under security:.

Verb → OpenAPI operation

Source Mapping
method: Operation key (get / post / put / delete / patch / head / options).
path: Appended to the block-level path: to form the OpenAPI path.
description: Operation summary:.
auto operationId: = <method>_<sanitised path>. Slashes, braces, and hyphens are replaced with underscores.
request-type: requestBody.content.<mime>.schema.$ref: "#/components/schemas/<RequestType>". Only emitted for POST / PUT / PATCH.
response-type: Same $ref: on the 200 response. When unset, the response schema is { "type": "object" }.
params: Each non-body entry → parameters[] with in: <type>, name:, required:, description:, schema:.
params: type=body Captured into requestBody.description. The body schema is the request-type: ref or { "type": "object" }.

Param data-type → JSON Schema type

data-type (YAML) OpenAPI schema.type
integer, int integer
number, float, double number
boolean, bool boolean
array array
any other (incl. empty) string

Auto-extracted path parameters

If path: "/users/{id}/orders/{orderId}" declares placeholders that aren't in the params: list, the generator still emits OpenAPI parameters[] entries for them, with in: path, required: true, and schema: { type: "string" }.

Security mapping

YAML OpenAPI securityScheme
type: basic { type: http, scheme: basic }
type: bearer, scheme: <s> { type: http, scheme: <s or "bearer"> }
type: apiKey, name: <n>, in: <i> { type: apiKey, name: <n or "X-API-Key">, in: <i or "header"> }
type: oauth2 { type: oauth2 } (flow details not yet supported)

The scheme name attached to each operation is built from security.name if set, otherwise <type>Auth (e.g. bearerAuth, apiKeyAuth, basicAuth, oauth2Auth).

What is not supported

This section is exhaustive — it covers features users coming from codegen-heavy OpenAPI workflows commonly expect but Helix doesn't implement.

  • Loading an OpenAPI spec into the engine. There is no openapi: top-level YAML key, no helix import-openapi, and no --openapi flag. A spec coming into the engine is a manual port to a rest: block.
  • Schema definitions (components.schemas). request-type: and response-type: produce $ref: entries, but the schemas themselves are not defined anywhere in the generated spec. Add the schema definitions yourself (e.g. via an OpenAPI post-processor) or document them externally.
  • Multiple response codes. Every operation emits only 200. No 204, 400, 404, 500, etc.
  • Per-operation security overrides. Security is block-level only — every verb shares the block's security: list.
  • Request body content types other than the verb's consumes. One requestBody.content entry per verb.
  • Examples. No example: / examples: emission.
  • Links / callbacks / webhooks / discriminators. Not generated.
  • OpenAPI 3.1. The generator emits openapi: "3.0.3" literally.
  • YAML output. Only JSON. The endpoint is /openapi.json; there is no /openapi.yaml.

Composition example — split spec across two rest: blocks

When you declare two rest: blocks with different tag: values they both contribute paths to the same /openapi.json (provided they share host+port). Useful when the public API and admin API share a server:

rest:
  - path: "/api/public"
    tag: "Public"
    description: "Public endpoints"
    verbs:
      - method: get
        path: "/health"
        target: direct://health

  - path: "/api/admin"
    tag: "Admin"
    description: "Admin endpoints (auth required)"
    security:
      - type: bearer
    verbs:
      - method: post
        path: "/users"
        target: direct://create-user
        request-type: "User"
        response-type: "User"

GET /openapi.json returns a single spec containing both /api/public/health and /api/admin/users, with Public and Admin in tags[].

Note that today only the first rest: block's /openapi.json route is registered (pkg/dsl/rest_reifier.go: buildOpenAPIRoute is called once per RestDefinition). When two blocks share host+port and only the second's spec is needed, declare the second-block verbs in the first block, or rely on the single spec that covers both.

See also

Source

  • pkg/dsl/rest_openapi.go — GenerateOpenAPI() and the openAPISpec types
  • pkg/dsl/rest_reifier.go — buildOpenAPIRoute() and the spec-serving processor
  • pkg/workflow/rest_definition.go — the input structs the generator reads