spac
Explanation

Philosophy

The design decisions behind the DSL

This page covers the how behind spac's design choices. For the why — the pain points spac exists to solve — see Why spac.

OpenAPI is the output, not the authoring experience

Most OpenAPI tools start with YAML or JSON and try to make editing it bearable. spac flips this: you write TypeScript, and OpenAPI is the output.

This means you get:

  • Type inference from TypeBox schemas — your editor knows the shape of every request and response
  • Refactoring — rename a type and every reference updates
  • Composition — use functions, loops, and variables to build specs programmatically
  • Testing — validate your spec with vitest alongside your application tests

No wrapper DSL

spac uses TypeBox schemas directly. There is no spac.string() or spac.object() — you use Type.String() and Type.Object() from TypeBox. This means:

  • Zero learning curve if you already know TypeBox
  • Full JSON Schema support out of the box
  • Schemas are portable — use them in runtime validation too

Named schemas and $ref hoisting

When you wrap a schema with named('Pet', schema), spac attaches a symbol to it. During emit, any named schema is:

  1. Hoisted to components.schemas
  2. Replaced with a $ref pointer wherever it appears

This happens recursively — nested named schemas in properties, items, allOf/oneOf/anyOf, etc. are all resolved.

Explicit versioning

The Api constructor requires a spec version as the first argument: new Api('3.1', ...). This is deliberate:

  • When OpenAPI 4.0 arrives, the library can gate features per version
  • In multi-team monorepos, Api<'3.1'> in function signatures provides compile-time version safety
  • Patch versions (3.1.0 → 3.1.2) are handled internally — you declare major.minor intent

Group inheritance

Tags and security set on a group automatically cascade to child routes. This mirrors how teams typically organize APIs — the /users group is tagged users and requires auth, so every route inside inherits that.

On this page