TypeScript DSL for authoring OpenAPI 3.1+ specs. Uses TypeBox for JSON Schema with full type inference — you write TypeScript, not YAML.
Docs · Playground · Getting Started · API Reference
import{Api,named}from'@spec-spac/spac'import{Type}from'@sinclair/typebox'constPet=named('Pet',Type.Object({id: Type.String(),name: Type.String()}))constapi=newApi('3.1','Petstore',{version: '1.0.0'})api.group('/pets',g=>{g.get('/').response(Type.Array(Pet)).summary('List all pets')g.post('/').body(Pet).response(Pet).summary('Create a pet')g.delete('/:id').params(Type.Object({id: Type.String()})).respond(204)})constspec=api.emit()// valid OpenAPI 3.1 JSONNamed schemas automatically hoist to components.schemas as $refs. Groups inherit tags and security. Macros let you compose reusable route/group patterns.
npm install @spec-spac/spac @sinclair/typeboxCreate api.ts:
import{Api,named}from'@spec-spac/spac'import{Type}from'@sinclair/typebox'constUser=named('User',Type.Object({id: Type.String(),name: Type.String(),email: Type.String({format: 'email'}),}))constapi=newApi('3.1','My API',{version: '1.0.0'})api.group('/users',g=>{g.tag('users')g.get('/').response(Type.Array(User)).summary('List users')g.get('/:id').params(Type.Object({id: Type.String()})).response(User)g.post('/').body(User).response(User).summary('Create user')})console.log(JSON.stringify(api.emit(),null,2))npx tsx api.ts > openapi.jsonThat's it — openapi.json is a valid OpenAPI 3.1 document with User hoisted to components.schemas.
| Package | Install | What it does |
|---|---|---|
@spec-spac/spac | npm i @spec-spac/spac | Core library — define routes, groups, schemas, emit OpenAPI 3.1 JSON/YAML with source maps |
@spec-spac/from-openapi | npm i -g @spec-spac/from-openapi | CLI + library — reverse-generate spac TypeScript from an existing OpenAPI spec |
@spec-spac/from-openapi-biome | npm i @spec-spac/from-openapi-biome | Biome formatter plugin for the code generator |
@spec-spac/from-openapi-prettier | npm i @spec-spac/from-openapi-prettier | Prettier formatter plugin for the code generator |
Already have an OpenAPI spec? Generate spac TypeScript from it:
# Preview what will be generated (dry run)
npx @spec-spac/from-openapi petstore.json
# Generate to a directory
npx spac-from-openapi petstore.json --out ./generated
# Strip path prefixes for cleaner grouping
npx spac-from-openapi cloudflare.json --out ./generated \
--strip '/accounts/{account_id}' \
--strip '/zones/{zone_id}'| Flag | Description |
|---|---|
--out <dir> | Output directory (omit for dry-run) |
--strip <prefix> | Path prefix to strip before grouping (repeatable) |
--name <name> | Override API title |
--spec-version <ver> | Override OpenAPI version |
--debug | Enable source map support in generated code |
Output structure:
generated/
index.ts — Api setup, imports all groups
shared/schemas.ts — Schemas used by 2+ groups
<group>/index.ts — Routes for that group
<group>/schemas.ts — Schemas local to that group
Every HTTP method returns a builder — all configuration is chained:
api.get('/users/:id').params(Type.Object({id: Type.String()})).query(Type.Object({fields: Type.Optional(Type.String())})).response(User).error(404,ErrorSchema).summary('Get a user by ID').operationId('getUser').tag('users')Groups collect routes under a shared prefix with inherited metadata:
api.group('/users',g=>{g.tag('users')g.security('bearer')g.get('/').response(Type.Array(User))g.post('/').body(CreateUser).response(User)g.get('/:id').params(Type.Object({id: Type.String()})).response(User)})Wrap any TypeBox schema with named() to hoist it to components.schemas:
constPet=named('Pet',Type.Object({id: Type.String(),name: Type.String()}))// Any route referencing Pet emits { "$ref": "#/components/schemas/Pet" }import{noContent,created,errorSchema}from'@spec-spac/spac'api.post('/items').body(CreateItem).respond(201,created(Item)).respond(204,noContent()).error(400,errorSchema('Bad request'))Reusable transforms applied via .use():
import{macro}from'@spec-spac/spac'constauthenticated=macro.route(r=>r.security('bearer'))constaudited=macro.group(g=>{g.tag('audited');g.security('bearer')})api.group('/admin',g=>{g.use(audited)g.get('/stats').response(Stats).use(authenticated)})Map every line of emitted YAML back to the TypeScript that produced it:
constapi=newApi('3.1','Petstore',{version: '1.0.0',debug: true})// ... define routes ...constresult=api.emit({sourceMap: true,generatedFile: 'spec.yaml'})result.yaml// YAML stringresult.sourceMap// standard Source Map V3result.sourceTable// Map<jsonPath, SourceEntry>| Method | Description |
|---|---|
new Api(specVersion, title, config?) | Create a new API spec (specVersion: '3.1') |
.get/.post/.put/.delete/.patch(path) | Top-level route (returns builder for chaining) |
.group(prefix, callback) | Group routes under a shared path prefix |
.server(config) | Add a server |
.securityScheme(name, config) | Register a security scheme |
.tag(config) | Register a tag |
.security(name) | Apply global security |
.use(macro) | Apply an API-level macro |
.emit() | Produce the OpenAPI 3.1 JSON document |
.emit({ yaml: true }) | Emit as YAML string |
.emit({ sourceMap: true }) | Emit YAML + Source Map V3 (requires debug: true) |
.params().query().headers().body().response().respond().error().summary().description().tag().operationId().deprecated().security().server().extension().use()
- Node.js 22+
- pnpm 10+
git clone https://github.com/rbbydotdev/spac.git
cd spac
pnpm installpackages/
spac/ — Core library (@spec-spac/spac)
from-openapi/ — CLI + library for reverse-generating spac from OpenAPI
playground/ — Interactive playground (Vite + React + CodeMirror)
website/ — Documentation site (Next.js + Fumadocs)
theme/ — Shared Shiki syntax themes
spac-vscode/ — VS Code extension
examples/ — Example projects (petstore, plantstore, serpapi, etc.)
# Build the core library
pnpm --filter @spec-spac/spac build
# Run core tests
pnpm --filter @spec-spac/spac test# Build + test the from-openapi package
pnpm --filter @spec-spac/from-openapi build
pnpm --filter @spec-spac/from-openapi testDocs site (Next.js):
pnpm --filter website dev
# → http://localhost:3000Playground (Vite):
# Generate fixtures first (required once, or after changing examples/spac source)
pnpm --filter spac-playground generate
# Start dev server
pnpm --filter spac-playground devThe generate step bundles type declarations, builds source maps, and creates example fixtures the playground needs. Re-run it after modifying example specs or the spac package.
# Install browser (first time only)
pnpm exec --filter spac-playground playwright install chromium
# Run tests
pnpm --filter spac-playground test:e2epnpm format # auto-fix with Biome
pnpm format:check # check only (used in CI)MIT

