Skip to content

Repository files navigation

spac

TypeScript DSL for authoring OpenAPI 3.1+ specs. Uses TypeBox for JSON Schema with full type inference — you write TypeScript, not YAML.

CInpmTypeScript 5.7+

Docs · Playground · Getting Started · API Reference

Hover tooltip showing .get() method signature and JSDoc in the plantstore example

Source mapping — TypeScript to OpenAPI YAML

Playground showing TypeScript source on the left mapped to generated OpenAPI YAML on the right


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 JSON

Named schemas automatically hoist to components.schemas as $refs. Groups inherit tags and security. Macros let you compose reusable route/group patterns.

Quick start

1. Install

npm install @spec-spac/spac @sinclair/typebox

2. Define your API

Create 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))

3. Run it

npx tsx api.ts > openapi.json

That's it — openapi.json is a valid OpenAPI 3.1 document with User hoisted to components.schemas.

Packages

PackageInstallWhat it does
@spec-spac/spacnpm i @spec-spac/spacCore library — define routes, groups, schemas, emit OpenAPI 3.1 JSON/YAML with source maps
@spec-spac/from-openapinpm i -g @spec-spac/from-openapiCLI + library — reverse-generate spac TypeScript from an existing OpenAPI spec
@spec-spac/from-openapi-biomenpm i @spec-spac/from-openapi-biomeBiome formatter plugin for the code generator
@spec-spac/from-openapi-prettiernpm i @spec-spac/from-openapi-prettierPrettier formatter plugin for the code generator

spac-from-openapi CLI

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}'
FlagDescription
--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
--debugEnable 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

Core concepts

Routes and chaining

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

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)})

Named schemas

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" }

Response helpers

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'))

Macros

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)})

Source maps

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>

API reference

MethodDescription
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)

RouteBuilder chaining

.params().query().headers().body().response().respond().error().summary().description().tag().operationId().deprecated().security().server().extension().use()

Contributing

Prerequisites

Setup

git clone https://github.com/rbbydotdev/spac.git
cd spac
pnpm install

Monorepo structure

packages/
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 & test

# 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 test

Running locally

Docs site (Next.js):

pnpm --filter website dev
# → http://localhost:3000

Playground (Vite):

# Generate fixtures first (required once, or after changing examples/spac source)
pnpm --filter spac-playground generate
# Start dev server
pnpm --filter spac-playground dev

The 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.

Playground e2e tests

# Install browser (first time only)
pnpm exec --filter spac-playground playwright install chromium
# Run tests
pnpm --filter spac-playground test:e2e

Formatting

pnpm format # auto-fix with Biome
pnpm format:check # check only (used in CI)

License

MIT

About

TypeScript DSL for authoring OpenAPI 3.1+ specs, write TypeScript - not YAML

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages