spac
Reference

API Reference

Complete type reference for the spac library

Creating an Api

const api = new Api('3.1', 'My API', {
  version: '1.0.0',
  description: 'A sample API',
  debug: true,
})

The first argument is the spec version (currently only '3.1'), the second is the API title, and the third is an optional config object:

Prop

Type


Defining Routes

HTTP methods are available on both Api and GroupBuilder. They accept only a path and return a RouteBuilder for chaining everything — request inputs, responses, and metadata.

api.get('/pets/:petId')
  .params(Type.Object({ petId: Type.String() }))
  .response(Pet)
  .error(404, ErrorSchema)
  .summary('Get a pet by ID')
  .tag('pets')

.params(schema) / .query(schema) / .headers(schema) / .cookies(schema)

Set request parameter schemas.

api.get('/pets')
  .query(Type.Object({ status: Type.Optional(Type.String()) }))

.body(schema)

Set the request body schema.

api.post('/pets')
  .body(CreatePet)
  .bodyDescription('The pet to create')
  .bodyRequired()

.response(schema)

Shorthand for a 200 response.

api.get('/pets').response(Type.Array(Pet))

.respond(status, schema)

Response for any status code. Accepts a StatusCode — a number, wildcard string, or 'default'.

type StatusCode = number | '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default'

The schema can be a TypeBox schema or a ResponseDef with metadata:

api.post('/pets')
  .body(CreatePet)
  .respond(201, Pet)
  .respond(201, {
    schema: Pet,
    description: 'Pet created',
    headers: { 'x-request-id': Type.String() },
  })

Prop

Type

HeaderConfig

Used within ResponseDef.headers for enriched header definitions:

Prop

Type

ExampleConfig

Used within ResponseDef.examples for named examples:

Prop

Type

.error(status, schema)

Error response for any StatusCode. Same status code type as .respond().

api.get('/pets/:petId')
  .params(Type.Object({ petId: Type.String() }))
  .response(Pet)
  .error(404, ErrorSchema)
  .error('5XX', ErrorSchema)

.link(name, config) / .link(status, name, config)

Attach an OpenAPI Link Object to a response. Without a status code, attaches to the 200 response.

api.get('/pets/:petId')
  .params(Type.Object({ petId: Type.String() }))
  .response(Pet)
  .link('GetOwner', {
    operationId: 'getOwner',
    parameters: { ownerId: '$response.body#/ownerId' },
  })

api.post('/pets')
  .body(CreatePet)
  .respond(201, Pet)
  .link(201, 'GetPetById', {
    operationId: 'getPet',
    parameters: { petId: '$response.body#/id' },
  })

Prop

Type

.summary(text) / .description(text) / .operationId(id)

api.get('/pets')
  .response(Type.Array(Pet))
  .summary('List all pets')
  .description('Returns a paginated list of pets with optional status filter.')
  .operationId('listPets')

.tag(name) / .tags(...names)

api.get('/pets').response(Type.Array(Pet)).tag('pets')
api.get('/admin/stats').response(Stats).tags('admin', 'analytics')

.deprecated()

api.get('/pets/findByTags').response(Type.Array(Pet)).deprecated()

.security(...schemes)

Operation-level security. Accepts scheme name strings or scope maps.

api.get('/pets')
  .response(Type.Array(Pet))
  .security('bearer')
  .security({ oauth2: ['read:pets'] })

.server(config)

Operation-level server override.

api.get('/health')
  .response(Type.Object({ status: Type.Literal('ok') }))
  .server({ url: 'http://localhost:3000', description: 'Health check only' })

.externalDocs(config)

api.get('/pets').response(Type.Array(Pet))
  .externalDocs({ url: 'https://docs.example.com/pets', description: 'Pet docs' })

.extension(name, value)

Add an x- prefixed extension field. The x- prefix is added automatically.

api.get('/pets').response(Type.Array(Pet))
  .extension('rate-limit', 100)     // emits x-rate-limit
  .extension('x-internal', true)    // kept as x-internal

.callback(name, expression, cb)

Register an out-of-band callback operation.

api.post('/webhooks/subscribe')
  .body(Type.Object({ url: Type.String({ format: 'uri' }) }))
  .respond(201, Subscription)
  .callback('onEvent', '{$request.body#/url}', g => {
    g.post('/')
      .body(Type.Object({ event: Type.String(), payload: Type.Unknown() }))
      .response(Type.Object({ received: Type.Boolean() }))
  })

.use(macro)

Apply a reusable RouteMacro. Multiple macros compose by chaining.

const authenticated = macro.route(r =>
  r.security('bearer').error(401, ErrorSchema).error(403, ErrorSchema)
)
const validated = macro.route(r => r.error(422, ValidationError))

api.post('/pets')
  .body(CreatePet)
  .response(Pet)
  .use(authenticated)
  .use(validated)

Groups

Groups collect routes under a shared path prefix. Tags, security, and extensions cascade to all child routes.

api.group('/pets', g => {
  g.tag('pets')
  g.security('bearer')

  g.get('/').response(Type.Array(Pet)).summary('List pets')
  g.post('/').body(CreatePet).respond(201, Pet).summary('Create a pet')

  g.group('/:petId', { params: Type.Object({ petId: Type.String() }) }, pet => {
    pet.get('/').response(Pet).summary('Get a pet')
    pet.delete('/').respond(204, noContent()).summary('Delete a pet')
  })
})

Group methods

MethodDescription
.tag(name)inherited by all routes
.security(...schemes)inherited by all routes
.description(text)group description
.server(config)group-level server override
.extension(name, value)x-* on all path items
.pathSummary(text)summary on path item object
.pathDescription(text)description on path item object
.use(macro)apply a GroupMacro
.group(prefix, cb)nested group

Group macros

const adminSection = macro.group(g =>
  g.tag('admin').security({ bearer: ['admin'] })
)

api.group('/admin', g => {
  g.use(adminSection)
  g.get('/stats').response(StatsResponse)
})

Api-level Methods

.server(config)

Add a server to the top-level servers array.

api.server({ url: 'https://api.example.com/v1', description: 'Production' })
api.server({
  url: 'https://{env}.example.com/v1',
  description: 'Configurable environment',
  variables: {
    env: { default: 'api', enum: ['api', 'staging', 'sandbox'] },
  },
})

Prop

Type

Prop

Type

.securityScheme(name, config)

Register a security scheme in components.securitySchemes.

api.securityScheme('bearer', { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' })
api.securityScheme('apiKey', { type: 'apiKey', name: 'X-API-Key', in: 'header' })
api.securityScheme('oauth2', {
  type: 'oauth2',
  flows: {
    authorizationCode: {
      authorizationUrl: 'https://example.com/oauth/authorize',
      tokenUrl: 'https://example.com/oauth/token',
      scopes: { 'read:pets': 'Read pets', 'write:pets': 'Modify pets' },
    },
  },
})

Prop

Type

Prop

Type

Prop

Type

.tag(config)

Add a tag to the top-level tags array. Accepts a string or a TagConfig object.

api.tag('pets')
api.tag({ name: 'pets', description: 'Everything about pets',
  externalDocs: { url: 'https://docs.example.com/pets' } })

Prop

Type

.schema(name, schema)

Register a named schema in components.schemas.

const Pet = named('Pet', Type.Object({ id: Type.Integer(), name: Type.String() }))
api.schema('Pet', Pet)

.security(...schemes)

Set global security requirements.

api.security('bearer')
api.security({ oauth2: ['read:pets'] })

.externalDocs(config)

api.externalDocs({ url: 'https://docs.example.com', description: 'Full documentation' })

.extension(name, value)

api.extension('x-logo', { url: 'https://example.com/logo.png' })

.webhook(name, cb)

Define a webhook (OpenAPI 3.1 webhooks).

api.webhook('petAdopted', g => {
  g.post('/').body(PetEvent).summary('Pet adopted notification')
})

.component(type, name, config)

Register arbitrary components (responses, parameters, headers, examples, links, callbacks, pathItems).

api.component('headers', 'X-Rate-Limit', { schema: Type.Integer() })
api.component('parameters', 'PageSize', { in: 'query', schema: Type.Integer() })

.use(macro)

Apply an ApiMacro.

const withServers = macro.api(a =>
  a.server({ url: 'https://api.example.com/v1', description: 'Production' })
   .server({ url: 'https://staging.example.com/v1', description: 'Staging' })
)
api.use(withServers)

Macros

Macros are reusable configuration functions created with macro.route(), macro.group(), or macro.api() and applied via .use().

Prop

Type

Prop

Type

Prop

Type


Version Policy

When multiple teams contribute modules to a single API, they may target different OpenAPI patch versions. The version policy system coordinates these declarations and catches mismatches at composition time.

Configuring the policy

Pass versionPolicy in the ApiConfig:

// Strict: error on any patch mismatch (e.g. 3.1.0 vs 3.1.2)
const api = new Api('3.1', 'My API', { versionPolicy: 'strict' })

// Warn (default): log warnings for patch mismatches, error on major.minor
const api = new Api('3.1', 'My API', { versionPolicy: 'warn' })

// Lenient: silently accept patch mismatches, error only on major.minor
const api = new Api('3.1', 'My API', { versionPolicy: 'lenient' })

Prop

Type

.assertVersion(version, label?)

Declare the OpenAPI version a team module was written for. Call this at the top of each team registration function.

export function registerCalls(api: Api<'3.1'>) {
  api.assertVersion('3.1.0', 'calls')
  api.group('/calls', g => { /* ... */ })
}

The comparison is asymmetric — the direction of the mismatch matters:

  • Major mismatch (e.g. 2.0 vs 3.1): always throws.
  • Team minor higher than central (e.g. team at 3.2, central at 3.1): always throws — the team may use features the output format doesn't support.
  • Team minor lower than central (e.g. team at 3.0, central at 3.1): generally safe — the team uses a subset of features. Handled per policy (same as patch mismatches).
  • Patch mismatch (e.g. 3.1.0 vs 3.1.2): depends on the active policy — 'strict' throws, 'warn' logs, 'lenient' is silent.

.versionAudit()

Audit all version declarations across team modules. Returns a VersionAudit object useful for CI checks.

const audit = api.versionAudit()
if (!audit.compatible) {
  console.error('Incompatible versions:', audit.errors)
  process.exit(1)
}

Prop

Type

Audit at emit time

Pass audit: true to .emit() to include the version audit in the result:

const result = api.emit({ audit: true })
console.log(result.versionAudit)
// { target: '3.1.2', policy: 'warn', declarations: [...], compatible: true, warnings: [...], errors: [] }

Emit

api.emit()                                   // JSON object
api.emit({ yaml: true })                     // { doc, yaml }
api.emit({ sourceTable: true })              // { doc, sourceTable }
api.emit({ sourceMap: true })                // { doc, yaml, sourceTable, sourceMap }
api.emit({ audit: true })                    // { doc, versionAudit }

Prop

Type


Helpers

named(name, schema)

Annotates a TypeBox schema for hoisting to components.schemas. Referenced via $ref in the output.

const Pet = named('Pet', Type.Object({ id: Type.Integer(), name: Type.String() }))

noContent(description?)

Creates a 204 No Content response definition.

api.delete('/pets/:petId')
  .params(Type.Object({ petId: Type.String() }))
  .respond(204, noContent())

// Custom description
.respond(204, noContent('Resource deleted'))

created(schema, description?)

Creates a 201 Created response definition.

api.post('/pets')
  .body(CreatePet)
  .respond(201, created(Pet))

// Custom description
.respond(201, created(Pet, 'Pet successfully created'))

errorSchema(fields?)

Standard error schema with message and optional code fields. Accepts additional fields.

// Basic error schema: { message: string, code?: string }
const Error = errorSchema()

// Extended with validation details
const DetailedError = errorSchema({
  details: Type.Array(Type.Object({
    field: Type.String(),
    issue: Type.String(),
  })),
})

api.get('/pets/:petId').response(Pet).error(404, Error)

paginated(schema)

Wraps a schema in { items, total, page, pageSize }.

api.get('/pets')
  .query(Type.Object({
    page: Type.Optional(Type.Integer({ default: 1 })),
    pageSize: Type.Optional(Type.Integer({ default: 20 })),
  }))
  .response(paginated(Pet))
// Response shape: { items: Pet[], total: number, page: number, pageSize: number }

envelope(schema)

Wraps a schema in { data: T }.

api.get('/pets/:petId')
  .params(Type.Object({ petId: Type.String() }))
  .response(envelope(Pet))
// Response shape: { data: Pet }

On this page