Skip to main content

Introduction

specforge

Forge production-ready client SDKs from OpenAPI 3.x specs.
One language-neutral IR. Emitters for TypeScript, Go, Rust, and WASM.
With runtime validation, documentation generation, and a plugin system.

Quick startLanguagesTestsVersionLicense

specforge mark


Why specforge?​

Most OpenAPI generators have critical shortcomings that leave teams building runtime infrastructure by hand:

Problem with other generatorsspecforge’s answer
Incomplete types — nullable, oneOf, allOf produce broken or any-typed outputFull composition support: allOf property merging, oneOf type guards, discriminator mapping, nullable propagation
No runtime — you get types but no client, auth, retry, or error handlingProduction-ready runtime: auth providers, exponential backoff, pagination helpers, concurrency control, middleware, idempotency keys, SSE streaming
Single language — each generator is a silo with different behaviorOne IR, four targets: TypeScript, Go, Rust, and WASM plugins share the same resolved spec
No validation — generated code trusts the server blindlyRuntime request/response validation catches contract violations in dev and tests
No testing — you write mock servers by handspecforge test generates mock server tests from example responses
No documentation — separate tools for API docsspecforge docs generates a static HTML documentation site
No CI integration — manual diffing and lintingspecforge diff detects breaking changes, specforge check lints specs, GitHub Action for one-line CI

The specforge advantage​

1. Parse once, emit many

Your OpenAPI spec is parsed and resolved into a language-neutral IR (Intermediate Representation). Every emitter — TypeScript, Go, Rust, or a custom WASM plugin — walks the same IR. This means:

  • Consistent behavior across languages (same retry logic, same pagination, same auth)
  • Adding a new language means writing one emitter, not re-implementing the parser
  • The IR is a documented, versioned JSON schema (assets/ir-schema.json)

2. SDKs that actually work in production

specforge doesn’t just generate types — it generates complete client libraries with:

  • Auth — Bearer tokens (static or dynamic), API keys, custom providers
  • Retry — Full-jitter exponential backoff with configurable max retries and per-attempt timeouts
  • Pagination — Cursor and offset pagination helpers that walk all pages automatically
  • Concurrency — Async semaphore to limit in-flight requests (maxConcurrent)
  • Deduplication — In-flight request coalescing for GET/HEAD/OPTIONS (one upstream call, N waiters)
  • Middleware — Composable request/response middleware chain (logging, tracing, header injection)
  • Idempotency — Automatic Idempotency-Key headers on POST/PUT/PATCH/DELETE
  • Streaming — SSE and chunk streaming helpers with proper error handling
  • Validation — Runtime request/response body validation against the spec

3. Multi-language with one source of truth

Generate SDKs for all your teams from the same spec:

graph LR
A[“📄 OpenAPI 3.x<br/>YAML / JSON”] --> B[“⚙️ specforge-core<br/>parse · resolve<br/>language-neutral IR”]
B --> C[“🔧 emitters<br/>TS · Go · Rust · WASM<br/>typed client + runtime”]

style A fill:#1a0f0a,stroke:#f97316,color:#fef3c7
style B fill:#1a0f0a,stroke:#ef4444,color:#fef3c7
style C fill:#1a0f0a,stroke:#fbbf24,color:#fef3c7

Each generated SDK is a standalone project — no shared runtime dependency, no version coupling. The TypeScript SDK is a dual ESM/CJS package, the Go SDK uses only stdlib, and the Rust SDK uses reqwest + serde.

4. Built for CI

  • specforge check lints your spec with configurable rules (.specforge.yaml)
  • specforge diff detects breaking changes between two spec versions — exit code 1 on breaking changes
  • GitHub Action for one-line CI integration
  • Deterministic output for reproducible builds and effective caching

5. Extensible via WASM plugins

Need Kotlin? Swift? Python? Build a custom emitter as a WASM plugin:

use specforge_plugin::{Plugin, PluginResult, GeneratedFile};

struct MyPlugin;
impl Plugin for MyPlugin {
fn generate(&self, ir_json: &str) -> PluginResult {
// Parse the IR, emit files for your language
}
}
specforge_plugin::export_plugin!(MyPlugin);

The plugin receives the full IR as JSON and returns generated files. Compile to WASM and run with specforge.


Features​

Spec parsing & resolution​

FeatureBenefit
OpenAPI 3.0 + 3.1Handles both spec versions transparently. 3.1 type arrays, $ref siblings, and numeric exclusiveMinimum are auto-converted to 3.0 for parsing.
Full $ref resolutionNamed types stay named — no exponential inlining blow-ups. Self-referential and mutual $ref cycles are safe by construction.
Composition supportallOf merges properties (last-wins, required union). oneOf/anyOf generate type guards. Discriminator mapping preserved.
AllOf type aliasesWhen allOf has one $ref member, Go emits embedded structs and Rust emits #[serde(flatten)] — proper composition, not flat merging.
Deterministic outputIndexMap preserves spec order. Same spec + same version = identical output. Bit-stable for caching and diffing.
Spec linting8 configurable rules (duplicate operation IDs, missing descriptions, unused schemas, etc.) with .specforge.yaml config.
Breaking change detectionspecforge diff compares two specs: removed operations, new required parameters, type changes. Exit code 1 for CI gates.

Generated SDK runtimes​

Every generated SDK is a complete, production-ready client — not just types.

CapabilityWhat it doesWhy it matters
Typed models + operationsFull request/response types with proper optionalityCatch type errors at compile time, not in production
Auth providersBearer (static/dynamic), API key (header/query), customSwap credentials without touching client code
Retry + backoffFull-jitter exponential backoff, configurable max retriesHandle transient failures gracefully without thundering herd
Per-attempt timeoutsConfigurable timeout on each retry attemptPrevent hung requests from blocking your app
Pagination helpersCursor and offset pagination that walks all pagesOne call instead of manual loop + cursor management
Concurrency semaphoremaxConcurrent limits in-flight requestsPrevent overwhelming the API or hitting rate limits
In-flight dedupeCoalesces identical GET/HEAD/OPTIONS requestsN concurrent callers → 1 upstream call, N shared results
Middleware chainComposable request/response middlewareAdd logging, tracing, header injection without modifying the client
Idempotency keysAuto-generated Idempotency-Key on unsafe methodsSafe retries on POST/PUT/PATCH/DELETE without duplicate side effects
SSE streamingServer-Sent Event parser with proper error handlingReal-time data streams without manual parsing
Runtime validationValidate request/response bodies against the specCatch API contract violations in dev and tests, not production
oneOf type guardsisPetCreated(), narrowPetEvent() (TS); is_pet_created(), discriminant() (Rust)Safely narrow union types at runtime

Language-specific highlights​

TypeScript

  • Dual ESM/CJS package (sideEffects: false for tree-shaking)
  • Native fetch — no runtime dependencies
  • Discriminated union error types (ApiError)
  • isX() / narrowX() type guards for oneOf unions
  • Per-model validatePet() functions

Go

  • Stdlib only (net/http, encoding/json) — zero third-party dependencies
  • Embedded structs for allOf composition
  • New{Union}(m map[string]any) for discriminated oneOf deserialization
  • New{Union}FromJSON(raw json.RawMessage) for non-discriminated unions
  • WithValidation(true) for runtime request/response checking

Rust

  • reqwest + serde + tokio async runtime
  • #[serde(flatten)] for allOf composition
  • impl PetEvent { fn discriminant() -> &str; fn is_pet_created() -> bool; } for oneOf
  • SseStream for SSE parsing over bytes_stream()
  • .validation(true) builder option

WASM plugins

  • specforge-plugin crate with Plugin trait and export_plugin! macro
  • Receives full IR as JSON, returns generated files
  • Compile to wasm32-wasi for any language emitter

CLI subcommands​

CommandWhat it does
specforge generateGenerate an SDK from an OpenAPI spec
specforge checkLint and validate a spec with configurable rules
specforge diffCompare two specs and report breaking changes
specforge emitDump the resolved IR as JSON (for external tools)
specforge initScaffold a new OpenAPI spec with a /health endpoint
specforge convertConvert between OpenAPI 3.0 and 3.1
specforge mergeMerge multiple spec files into one
specforge migrateGenerate a migration guide between two spec versions
specforge docsGenerate a static HTML API documentation site
specforge testGenerate mock server tests from spec examples
specforge versionsList API versions in a spec directory
specforge workspaceGenerate SDKs for all specs in a workspace config

Quality gates​

  • 169+ unit tests across core, emitters, and CLI
  • Regression suite on petstore (vendored) + GitHub / Stripe (large real-world specs)
  • Compile gates: generated Go must go build, generated Rust must cargo check
  • E2E smoke: mock server × list/show/create + auth/retry/pagination (all 3 langs)
  • E2E advanced: concurrency serialisation, dedupe single-flight, middleware rewrite, idempotency-key on POST, SSE parse (all 3 langs)
  • Multi-platform CI: Linux, macOS, Windows (GitHub Actions matrix)
  • Cross-compiled releases: 5 targets (linux amd64/arm64, macOS Intel/Apple Silicon, Windows)

Quick start​

1. Build the CLI​

cargo build -p specforge-cli
# → target/debug/specforge

2. Generate an SDK​

# TypeScript (default) — native fetch, dual ESM/CJS package
./target/debug/specforge generate openapi.yaml -o ./sdk-ts -l ts

# Go — stdlib net/http only
./target/debug/specforge generate openapi.yaml -o ./sdk-go -l go \
-n github.com/acme/widget-go

# Rust — reqwest + serde
./target/debug/specforge generate openapi.yaml -o ./sdk-rs -l rust \
-n widget_sdk

# Lint a spec without generating
./target/debug/specforge check openapi.yaml
./target/debug/specforge check openapi.yaml --strict

3. Call your API​

TypeScript
import { createClient, bearerAuth, streamSse } from "./sdk-ts/src/index.ts";

const client = createClient({
baseUrl: "https://api.example.com",
auth: bearerAuth(() => process.env.API_TOKEN!),
maxConcurrent: 8,
dedupe: true,
idempotency: true,
retry: { maxRetries: 3 },
});

const page = await client.pets.listPets({ limit: 20 });

// Streaming (SSE)
const res = await client.request("GET", "/events");
for await (const ev of streamSse(res)) {
console.log(ev.event, ev.data);
}
Go
c := sdk.NewClient().
WithBaseURL("https://api.example.com").
WithBearerToken(os.Getenv("API_TOKEN")).
WithTimeout(10 * time.Second).
WithMaxConcurrent(8).
WithDedupe(true).
WithIdempotency(true).
WithRetry(sdk.DefaultRetryOptions())

c.Use(func(ctx context.Context, req *sdk.MiddlewareRequest, next func(context.Context, *sdk.MiddlewareRequest) (*sdk.MiddlewareResponse, error)) (*sdk.MiddlewareResponse, error) {
start := time.Now()
res, err := next(ctx, req)
log.Printf("%s %s %v", req.Method, req.URL, time.Since(start))
return res, err
})

pets, err := c.ListPets(ctx, 20)

// Streaming SSE
res, err := c.DoStream(ctx, "GET", "/events", nil, nil)
defer sdk.DrainAndClose(res)
it := sdk.NewSseIterator(res)
for it.Next() {
ev := it.Event()
fmt.Println(ev.Event, ev.Data)
}
Rust
use std::time::Duration;
use widget_sdk::{api, Client};
use widget_sdk::streaming::SseStream;

let client = Client::builder()
.base_url("https://api.example.com")
.bearer_token(std::env::var("API_TOKEN")?)
.timeout(Duration::from_secs(10))
.max_concurrent(8)
.dedupe(true)
.idempotency(true)
.build()?;

let pets = api::list_pets(&client, Some(20)).await?;

// Streaming SSE
let res = client.request_stream(reqwest::Method::GET, "/events", &[], None).await?;
let mut sse = SseStream::new(res.bytes_stream());
while let Some(ev) = sse.next_event().await? {
println!("{}: {}", ev.event, ev.data);
}

CLI reference​

specforge <COMMAND>

Commands:
generate Generate an SDK from an OpenAPI spec
check Lint and validate an OpenAPI spec without generating
diff Compare two OpenAPI specs and report breaking changes
emit Emit the resolved IR as JSON (for external emitters / plugins)
init Scaffold a new minimal OpenAPI spec
convert Convert between OpenAPI 3.0 and 3.1
merge Merge multiple OpenAPI spec files into one
migrate Generate a migration guide between two spec versions
docs Generate static HTML API documentation
test Generate mock server tests for generated SDKs
versions List API versions in a spec directory
workspace Generate SDKs for all specs in a workspace config
workspace-init Generate a workspace config from a directory
help Print this message or the help of the given subcommand

specforge generate​

specforge generate [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to OpenAPI YAML or JSON

Options:
-o, --out <DIR> Output directory [default: ./generated]
-l, --lang <LANG> ts | go | rust [default: ts]
-n, --name <NAME> Package / module / crate name override
--version <VERSION> API version (when spec is a directory)
--profile Output timing breakdown for each pipeline stage
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]
-h, --help
-V, --version
Language-n meansDefault if omitted
tsnpm package name in package.json@<title-slug>sdk
goGo module path in go.modgithub.com/example/<title>-go
rustCargo crate name<title>_sdk

specforge check​

specforge check [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to OpenAPI YAML or JSON

Options:
--strict Treat warnings as errors
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

specforge diff​

specforge diff [OPTIONS] <OLD> <NEW>

Arguments:
<OLD> Path to the old (baseline) OpenAPI spec
<NEW> Path to the new OpenAPI spec

Options:
--breaking-only Show only breaking changes
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

Exit code 1 if breaking changes are found — use in CI to gate releases.

specforge emit​

specforge emit [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to OpenAPI YAML or JSON

Options:
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: warn]

Outputs the resolved IR as pretty-printed JSON to stdout. Use this to build external emitters:

specforge emit openapi.yaml | my-custom-emitter --input - --output ./sdk

specforge docs​

specforge docs [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to OpenAPI YAML or JSON

Options:
-o, --out <DIR> Output directory [default: ./docs]
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

Generates a static HTML documentation site with color-coded HTTP method badges, schema listings, and the base URL.

specforge test​

specforge test [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to OpenAPI YAML or JSON

Options:
-o, --out <DIR> Output directory [default: ./tests]
-l, --lang <LANG> ts | go | rust [default: ts]
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

Generates mock server test files that start a local HTTP server from the spec's example responses and verify the SDK can call each operation. Supports TypeScript (http module), Go (httptest), and Rust (TcpListener).

specforge versions​

specforge versions [OPTIONS] <SPEC>

Arguments:
<SPEC> Path to a spec file or directory containing versioned specs

Options:
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

Lists all API versions found in a directory. Supports flat (v1.yaml, v2.yaml) and nested (v1/openapi.yaml, v2/openapi.yaml) conventions. Use with specforge generate specs/ --version v2 to generate for a specific version.

specforge migrate​

specforge migrate [OPTIONS] <OLD> <NEW>

Arguments:
<OLD> Path to the old (baseline) OpenAPI spec
<NEW> Path to the new OpenAPI spec

Options:
-o, --out <FILE> Output file (default: stdout)
-v, --log-level <LEVEL> error|warn|info|debug|trace [default: info]

Generates a migration guide in Markdown format comparing two spec versions. Lists deprecated operations, removed operations, new required parameters, and schema changes.


Architecture​

specforge is a Cargo workspace with a hard boundary: emitters never touch openapiv3 types. They only see the IR.

specforge/
├── crates/
│ ├── specforge-core/ # parse · $ref resolve · IR
│ ├── specforge-ts/ # TypeScript emitter + rich runtime templates
│ ├── specforge-go/ # Go emitter (stdlib HTTP client)
│ ├── specforge-rust/ # Rust emitter (reqwest + serde)
│ ├── specforge-wasm/ # WASM target for browser-based parsing
│ ├── specforge-plugin/ # Plugin SDK for WASM emitter plugins
│ └── specforge-cli/ # `specforge` binary + regression / e2e tests
├── fixtures/
│ ├── petstore.yaml # small vendored fixture (always online)
│ └── sample-api.yaml # auth · oneOf · cursor pagination sample
├── examples/ # consumer stubs (regenerate via script)
├── scripts/
│ ├── ci.sh # local CI mirror
│ └── generate-examples.sh
├── assets/ # logo + banner
├── CHANGELOG.md
├── RELEASE.md
└── .github/workflows/ci.yml

Pipeline stages​

StageCrateResponsibility
Parsespecforge-coreYAML/JSON → openapiv3::OpenAPI
Resolvespecforge-core$refs, security schemes, operations → Document IR
Emitspecforge-{ts,go,rust}IR → idiomatic project on disk
WASMspecforge-wasmCore parsing/resolving compiled to WASM for browser use
Pluginsspecforge-pluginSDK for building custom WASM emitter plugins
Orchestratespecforge-cliCLI flags, logging, language dispatch

IR highlights​

// Simplified view of crates/specforge-core/src/ir.rs
pub struct Document {
pub title: String,
pub version: String,
pub base_url: Option<String>,
pub security: Vec<SecurityScheme>,
pub schemas: SchemaRegistry, // named models, stable order
pub operations: Vec<Operation>,
}

pub enum Type {
Scalar(Scalar),
StringEnum { variants: Vec<String>, nullable: bool },
Array { item: Box<Type>, nullable: bool },
Map { value: Box<Type> },
Reference { name: String, nullable: bool },
Composition(Composition), // allOf | oneOf | anyOf + optional Discriminator
Any,
Unknown,
}

Design rules that keep large specs tractable (GitHub ~965 schemas / 1209 ops, Stripe ~1431 / 587):

  • References stay references — no exponential inlining
  • Cycles are safe — self-$ref becomes a plain name
  • Determinism — IndexMap preserves spec order for bit-stable output

Generated project layouts​

TypeScript -l ts
sdk-ts/
├── package.json # dual ESM/CJS, sideEffects: false
├── tsconfig.json # strict TS 5.6+
├── tsup.config.ts
├── README.md
└── src/
├── index.ts # createClient() + re-exports
├── client.ts # fetch core
├── auth.ts
├── retry.ts
├── paginate.ts
├── concurrency.ts # async semaphore
├── dedup.ts # in-flight GET coalescing (buffered bodies)
├── middleware.ts
├── idempotency.ts # Idempotency-Key generation
├── streaming.ts # streamBytes / streamLines / streamSse
├── errors.ts # discriminated-union ApiError
├── models/<Name>.ts # one file per schema (+ oneOf guards)
└── api/<Tag>.ts # one class per tag
Go -l go
sdk-go/
├── go.mod
├── client.go # Client, auth, DoJSON + DoStream pipeline
├── retry.go
├── paginate.go # CursorPaginate / OffsetPaginate generics
├── concurrency.go # Semaphore
├── dedup.go # RequestDeduper
├── middleware.go
├── idempotency.go # Idempotency-Key UUID
├── streaming.go # NewSseIterator / StreamLines
├── models.go
├── api_<tag>.go # methods on *Client
└── README.md

Zero third-party deps for the happy path (net/http, encoding/json).

Rust -l rust
sdk-rs/
├── Cargo.toml # reqwest · serde · tokio · futures-util · bytes
├── README.md
└── src/
├── lib.rs
├── api/ # one module per tag, one fn per operation
├── cache.rs # ETag/conditional GET response cache
├── client.rs # Client + ClientBuilder + ServiceContainer + request_stream
├── concurrency.rs # async in-flight semaphore
├── dedup.rs # coalesce identical in-flight safe requests
├── error.rs # typed Error enum
├── idempotency.rs # auto Idempotency-Key for safe retries
├── interceptors.rs # request/response body interceptors
├── logging.rs # pluggable Logger trait + Console/Noop impls
├── middleware.rs # request/response middleware chain
├── models.rs # one struct/enum per schema + validators
├── paginate.rs # cursor/offset pagination helpers
├── ratelimit.rs # token-bucket + sliding-window rate limiters
├── retry.rs # exponential backoff + jitter
├── streaming.rs # SseStream for server-sent events
├── telemetry.rs # TelemetryHooks trait + MetricsCollector
├── validate.rs # generated per-model validators
└── validation_middleware.rs # schema validation as middleware
WASM plugin -l wasm (via specforge-plugin)
plugin/
├── Cargo.toml # crate-type = ["cdylib"]
├── src/
│ └── lib.rs # impl Plugin + export_plugin! macro
└── README.md

Build: cargo build --target wasm32-wasi --release


Runtime deep dive​

All three clients share the same conceptual request pipeline:

graph TD
A["🚀 operation call"] --> B["build URL / query / body"]
B --> C["acquire concurrency permit<br/>(optional)"]
C --> D{"dedupe in-flight?<br/>GET/HEAD/OPTIONS"}
D -->|yes| E["share result with<br/>concurrent callers"]
D -->|no| F["retry loop"]
F --> G["apply auth"]
G --> H["attach Idempotency-Key<br/>(unsafe methods, once per loop)"]
H --> I["per-attempt timeout"]
I --> J["middleware chain"]
J --> K["transport<br/>fetch / net/http / reqwest"]
K --> L{"classify error"}
L -->|retriable| F
L -->|success| M["decode JSON → typed model"]
L -->|non-retriable| N["throw"]
E --> M

style A fill:#1a0f0a,stroke:#f97316,color:#fef3c7
style M fill:#1a0f0a,stroke:#22c55e,color:#fef3c7
style N fill:#1a0f0a,stroke:#ef4444,color:#fef3c7

Auth​

LanguageStatic bearerDynamic tokenAPI key header
TSbearerAuth(() => token)async getter supportedapiKeyAuth(header, getKey)
GoWithBearerToken(tok)BearerAuth{GetToken: …}WithAPIKey(header, key)
Rust.bearer_token(tok)Auth::BearerFn(…).api_key(header, key)

Retry defaults​

  • Max retries: 2 (3 total attempts)
  • Backoff: full jitter, base 500ms, cap 8s
  • Retriable statuses: 408, 429, 502, 503, 504
  • Retriable methods: GET, HEAD, PUT, DELETE, (OPTIONS) + transport/timeouts

Idempotency​

Unsafe methods (POST / PUT / PATCH / DELETE) automatically receive a stable Idempotency-Key for the entire retry loop (one key generated, reused on retries). Disable with idempotency: false / WithIdempotency(false) / .idempotency(false).

Streaming (SSE)​

LanguageEntry pointParser
TSclient.request("GET", "/events")streamSse(res) / streamLines / streamBytes
Goclient.DoStream(ctx, "GET", "/events", …)NewSseIterator(res)
Rustclient.request_stream(GET, "/events", …)SseStream::new(res.bytes_stream())

Streaming calls do not retry (bodies are not replayable).

oneOf narrowing (TypeScript)​

import { isPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent";

function handle(event: PetEvent) {
if (isPetCreated(event)) {
console.log(event.pet.name); // narrowed
return;
}
switch (narrowPetEvent(event)) {
case "PetUpdated": /* … */ break;
case "PetDeleted": /* … */ break;
}
}

Examples​

Ready-to-run consumer stubs live under examples/:

./scripts/generate-examples.sh # regenerate sdk/ trees from fixtures/petstore.yaml

cd examples/petstore-ts && npm i && npx tsx main.mts
cd examples/petstore-go && go run .
cd examples/petstore-rust && cargo run

Point them at your own server with PETSTORE_URL=….

Repo fixtures for generator development:

FixtureWhat it stresses
fixtures/petstore.yamlSmall happy path — list / show / create
fixtures/sample-api.yamlBearer auth, enums, oneOf + discriminator, cursor pages

Testing & CI​

Local​

# Full suite — build, unit, large-spec compile gates, e2e (smoke + advanced), tsc
./scripts/ci.sh

# Faster loops
./scripts/ci.sh quick # unit + petstore generate
./scripts/ci.sh regression # includes GitHub/Stripe go build + cargo check
./scripts/ci.sh e2e # smoke + advanced e2e

What each suite proves​

SuiteCommandCoverage
Unitcargo test -p specforge-core --all-targets
cargo test -p specforge-ts --lib
IR construction, TS naming/types/models/ops
Regressioncargo test -p specforge-cli --test regressionPetstore generate (TS/Go/Rust); GitHub + Stripe resolve; Go/Rust generate + compile on large specs
E2E smokecargo test -p specforge-cli --test e2e_smokePetstore basics + sample-api auth / 503-retry / cursor pagination (3 langs)
E2E advancedcargo test -p specforge-cli --test e2e_advancedConcurrency serialisation, dedupe single-flight, middleware header rewrite, idempotency-key on POST, SSE parse (3 langs)

Large specs download on demand into target/spec-cache/ and are skipped (not failed) when offline. Compile steps skip only if go / cargo are missing; a failed compile when the tool is present fails CI.

GitHub Actions​

.github/workflows/ci.yml runs on every push/PR:

  • test — full suite (Rust stable + Go 1.23 + Node 20, cached cargo + spec-cache)
  • quick — build + unit + petstore-only regression for a fast signal

Release​

See RELEASE.md for the full checklist. Short version:

# 1. bump [workspace.package].version in Cargo.toml
# 2. update CHANGELOG.md
./scripts/ci.sh full
./scripts/generate-examples.sh
git tag -a v0.1.0 -m "specforge v0.1.0"
git push origin v0.1.0

Binary: cargo build --release -p specforge-cli → target/release/specforge.


Requirements​

ToolRequired for
Rust 1.75+ (rustc/cargo)Building specforge itself; Rust SDK cargo check
Go 1.22+Go SDK go build + e2e (optional but recommended)
Node 20+ / npmTypeScript tsc + e2e (optional but recommended)

Status & roadmap​

v1.0.0 — rate limiting, telemetry, deprecation, website:

  • SDK rate limiting — token bucket + sliding window (TS/Go/Rust)
  • Telemetry hooks — request metrics, error tracking, cache hit/miss
  • specforge migrate — generate migration guides between spec versions
  • Deprecation tracking + comments in generated code
  • 6 external test fixtures (GitHub, Stripe, Kubernetes, Twilio)
  • Website at specforge.deepwhaleai.com
  • Go pipeline fixes (validate.go, hyphenated fields)
  • Test count: 176

v0.9.0 — caching, webhooks, workspace:

  • Response caching with ETags (TS/Go/Rust SDKs)
  • OpenAPI 3.1 webhooks support (--include-webhooks)
  • specforge workspace — multi-spec generation from config
  • specforge workspace-init — generate workspace config from directory

v0.8.0 — DI, merge, 3.1 expansion:

  • Rust http_client() builder for dependency injection in tests
  • specforge merge — combine multiple spec files into one
  • OpenAPI 3.1 expanded: const, dependentRequired, prefixItems
  • 3.1 feature detection (detect_31_features())

v0.7.0 — tree-shaking, versioning, profiling:

  • Tree-shakeable TS API modules (per-tag imports, src/api/index.ts barrel)
  • specforge versions — list API versions in a spec directory
  • --version flag on generate — filter by version from directory
  • --profile flag on generate and emit — timing breakdown

v0.6.0 — testing, 3.1, lint config:

  • specforge test — mock server test generation (TS/Go/Rust)
  • OpenAPI 3.1 $ref sibling support (description/summary via allOf wrapping)
  • Configurable lint rules (.specforge.yaml, --disable/--enable/--severity)
  • New lint rules: missing-operation-id, path-trailing-slash, deprecated-operation

v0.5.0 — validation, WASM, docs:

  • Runtime validation middleware (TS/Go/Rust SDKs)
  • WASM-compiled specforge (specforge-wasm crate)
  • specforge docs — static HTML API documentation generator
  • Web UI WASM integration (client-side parsing)

v0.4.0 — plugins, web UI, validation:

  • WASM plugin SDK (specforge-plugin crate + export_plugin! macro)
  • Example WASM plugin (examples/plugin-example/)
  • Web UI for browsing IR (web-ui/index.html)
  • Spec validation middleware (validate module, 51 tests)
  • Plugin documentation (PLUGINS.md)

v0.3.0 — ecosystem, DX, and stability:

  • JSON Schema for IR (assets/ir-schema.json, emit --schema)
  • Streaming IR emission (emit --stream — NDJSON for large specs)
  • Parallel file generation (rayon)
  • specforge init — scaffold new OpenAPI specs
  • specforge convert — 3.0 ↔ 3.1 conversion
  • GitHub Action (action.yml — generate/check/diff/emit)
  • VS Code extension scaffolding
  • Stability policy (STABILITY.md)
  • Incremental generation guide (INCREMENTAL.md)

v0.2.2 — OpenAPI 3.1, plugin system, mascot:

  • OpenAPI 3.1 support — transparent type array → nullable conversion, numeric exclusiveMinimum → boolean
  • specforge emit — dump resolved IR as JSON for external emitters / plugins
  • Fox blacksmith mascot + forge color scheme

v0.2.1 — allOf composition, spec diff, and benchmarks:

  • AllOf type aliases — Go embed, Rust #[serde(flatten)]
  • specforge diff <old> <new> — breaking-change detection for CI
  • Generation benchmarks (scripts/bench.sh) — GitHub/Stripe specs

v0.2.0 — composition, ergonomics, and release infrastructure:

  • Discriminator mapping support (all 3 emitters)
  • Go oneOf helpers (New{Union}, {Union}Discriminant, New{Union}FromJSON)
  • Rust oneOf helpers (discriminant(), is_*(), into_*(), as_*())
  • AllOf property merging (last-wins, required union)
  • Spec linting (specforge check [--strict])
  • Go + Rust streaming middleware (StreamMiddleware)
  • Cross-compiled release binaries (5 targets)
  • Crates.io publish workflow
  • Multi-platform CI (Linux, macOS, Windows)
  • Richer generated READMEs (errors, pagination, concurrency, middleware, streaming)

All roadmap items completed through v1.2.0.


Ecosystem​

GitHub Action​

Use specforge in CI with the official GitHub Action:

- uses: amafjarkasi/specforge-openapi-sdk-codegen@v0.2.2
with:
command: generate
spec: openapi.yaml
lang: ts
output: ./sdk

Commands: generate, check, diff, emit

VS Code Extension​

Install the specforge VS Code extension for:

  • One-click SDK generation
  • Spec validation
  • IR preview

External Emitters​

Build custom emitters using specforge emit:

# Pipe the IR to your custom emitter
specforge emit openapi.yaml | my-emitter --lang kotlin --output ./sdk

# Stream mode for large specs
specforge emit openapi.yaml --stream | while read -r line; do
echo "$line" | process-schema
done

See assets/ir-schema.json for the IR JSON Schema.

Deterministic Output​

specforge guarantees deterministic output — the same spec + version always produces identical files. Use this for:

  • CI caching (hash spec + version as cache key)
  • Reproducible builds
  • Diff-friendly generated code

See INCREMENTAL.md for CI caching strategies.


Contributing​

git clone <repo-url> && cd specforge
cargo build --workspace
./scripts/ci.sh quick # before you start
# … hack …
./scripts/ci.sh # before you push

Guidelines:

  1. Emitters only see the IR — never import openapiv3 outside specforge-core.
  2. Keep output deterministic — prefer BTreeMap / IndexMap over HashMap for emission order.
  3. Don’t nest /* */ inside JSDoc/templates — it breaks generated TypeScript.
  4. Large-spec gates are sacred — if GitHub/Stripe stop compiling, fix the emitter, don’t skip.
  5. Dedupe must hand each waiter a fresh body — never share one consumed Response.

License​

MIT © specforge contributors

Commercial Use​

specforge is released under the MIT License, which permits commercial use, modification, distribution, and private use without restriction. You are free to:

  • Use specforge in proprietary commercial products
  • Include specforge in closed-source commercial applications
  • Sell products built with or powered by specforge
  • Use specforge in enterprise internal tools
  • Modify specforge for commercial purposes

No attribution is required in the MIT license, though attribution is appreciated.

No copyleft restrictions — unlike GPL, the MIT license does not require you to open-source your own code when using specforge.

Commercial support is available by contacting the maintainers at specforge.deepwhaleai.com.



OpenAPI in. Typed clients out. Ship faster.