Stone.jsDocs
Paradigm

Extensions

OpenAPI

A public API deserves a public contract, but a contract maintained by hand drifts from the code the day after it is written. @stone-js/openapi derives the document from the schemas your validation and resources already define, so it stays true by construction.

#Install

terminalbash
npm i @stone-js/openapi

#Derive, do not duplicate

The principle

Two descriptions of the same API, the code and a hand-written spec, cannot stay in sync. Keep one description, the code, and generate the other from it. The contract becomes a view of the implementation, not a parallel artifact.

In Stone.js

OpenApiGenerator builds a valid OpenAPI document from your app: the info block, servers, and the paths and schemas taken from your routes, validation and resources. Serve it at a URL and point any tool at it.

app/openapi.tsts
import { OpenApiGenerator } from '@stone-js/openapi'

export const spec = OpenApiGenerator
  .create({ title: 'Tasks API', version: '1.0.0' })
  .addServer('https://api.example.com')
  .build()   // a valid OpenAPI document

#What is derived, and from where

Everything a route already says about itself, read from the route and from the handler's own decorators, because both validation and resources work without a router, and a contract that only read route options documented half of such an application.

DerivedTypeDescription
RequestvalidationFrom `validation:` on the route or `@Validate()` on the handler.
ResponseresourcesFrom `resource:` on the route or `@Returns()` on the handler: the schema the resource publishes is the documented payload.
FragmentsresourcesA query parameter with an enum of the names a caller may select, the parameter your app actually answers to.
Securityauth / authzFrom `auth:` / `authz:` on the route, or `@Protect()` / `@Can()` on the handler.

A declaration it could not read is reported rather than dropped: a resource named but never registered, or a schema that needs a real context, prints one line naming the route. A missing payload in a document that looks complete is worse than a loud gap.

#Saying more, on the route

A route can add anything the derivation cannot know (a summary, tags, extra responses) under contract. What you write there wins, because an author who wrote it meant it, and contract: false keeps an endpoint out of the document entirely.

app/TasksController.tsts
@Get('/tasks', { contract: { summary: 'List tasks', tags: ['tasks'] } })
list () {}

@Get('/internal/metrics', { contract: false })   // documented nowhere
metrics () {}

#Serving it

You do not have to wire any of it. Enable the module the way every Stone.js module is enabled, with its decorator or with its blueprint, and two routes appear: /openapi.json for the document and /docs for its explorer. Both paths are configurable under stone.openapi, and docsPath: false serves the machine-readable contract alone, for when the explorer must not be public.

app/Application.tsdeclarativeimperative
import { OpenApi } from '@stone-js/openapi'
import { StoneApp } from '@stone-js/core'

@OpenApi({ info: { title: 'Tasks API', version: '1.0.0' } })
@StoneApp({ name: 'my-app' })
export class Application {}
import { defineStoneApp } from '@stone-js/core'
import { openApiBlueprint } from '@stone-js/openapi'

export const Application = defineStoneApp(handler, { name: 'my-app' }, [openApiBlueprint])

#Serving it yourself

If your application assembles its own document, or must serve it from somewhere the module does not reach, expose the built document from a route of your own. Tools, machines and agents read it directly; point any OpenAPI viewer (Swagger UI, Scalar) at that URL.

app/OpenApiController.tsdeclarativeimperative
import { EventHandler, Get } from '@stone-js/router'
import { spec } from './openapi'

@EventHandler('/openapi.json')
export class OpenApiController {
  @Get('/')
  document () { return spec }   // the generated OpenAPI document, as JSON
}
import { defineEventHandler, defineRoutes } from '@stone-js/router'
import { spec } from './openapi'

const OpenApiController = () => ({ document: () => spec })

export const routes = defineRoutes([
  [defineEventHandler(OpenApiController, 'document'), { path: '/openapi.json', method: 'GET' }]
])

#Schemas to JSON Schema

Under the hood toJsonSchema converts your validation schemas (Zod and other Standard Schemas) into the JSON Schema the document embeds, which is why the contract stays a faithful view of what you actually validate.

You wrote the schema for validation. The contract is a view of it, not a second copy.

A request is described as input and a response as output, which is both the honest direction and the only one that works: a request is what the caller sends, before coercion and defaults, and a schema that normalises before it judges (a trimmed handle, an upper-cased country) has no output shape to describe at all. Asking for one throws, and that used to take the whole document with it.

Two details fall out of the same rule. The document declares OpenAPI 3.0, so that dialect is requested from the schema engine (a nullable string is nullable: true, not a union with null) and a $schema marker never reaches the output, because 3.0 has nowhere to put one. And a route with no name publishes no operationId at all, rather than an empty one that generated clients key off.


Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)