OpenAPI annotations
Accorudo types carry OpenAPI metadata through JSDoc. Annotations use the @openapi.* namespace. They are comments only (erased at compile time, zero bundle cost) and read by accorudo export (and preserved by accorudo import where possible).
Place annotations on types, properties, or route aliases depending on what they describe.
Quick reference
| Annotation | Applies to | OpenAPI target |
|---|---|---|
@openapi.component | type | components.schemas |
@openapi.component parameters | type | components.parameters |
@openapi.component responses | type | components.responses |
@openapi.component requestBodies | type | components.requestBodies |
@openapi.component securitySchemes | type | components.securitySchemes |
@openapi.type | property | JSON Schema type |
@openapi.format | property | JSON Schema format |
@openapi.description | type or property | description |
@openapi.example | type or property | example |
@openapi.nullable | property | nullable: true |
@openapi.required | property | add to required |
@openapi.inline | property or generic | inline schema instead of $ref |
@openapi.in | path/query param field | parameter in |
@openapi.response CODE | route alias | additional response |
@openapi.operationId | route alias | operationId |
@openapi.tag | route alias | operation tags |
@openapi.security | route alias | operation security |
@openapi.deprecated | route alias or property | deprecated: true |
Components
Register a reusable schema under components.schemas. Referencing a component type elsewhere in the bundle emits a $ref by default. No extra annotation needed.
/**
* @openapi.component
* @openapi.description A widget in the catalog.
*/
export type Widget = {
/**
* @openapi.type integer
* @openapi.format int64
*/
id: number;
name: string;
/** @openapi.nullable */
description?: string | null;
};
Exported OpenAPI:
components:
schemas:
Widget:
description: A widget in the catalog.
type: object
required: [id, name]
properties:
id:
type: integer
format: int64
name:
type: string
description:
type: string
nullable: true
The type name (Widget) becomes the component key. Use PascalCase. It matches Accorudo model conventions and reads cleanly in $ref paths.
Other component buckets
Use a second token on @openapi.component for non-schema components:
/**
* @openapi.component parameters
* @openapi.in query
* @openapi.name limit
* @openapi.description Page size.
*/
export type LimitParameter = number;
/**
* @openapi.component responses
*/
export type NotFoundResponse = {
errors: ErrorDetail[];
};
/**
* @openapi.component requestBodies
*/
export type CreateWidgetBody = {
name: string;
enabled: boolean;
};
Reference reusable parameters and responses from route annotations (see Responses below).
Field annotations
TypeScript types do not always map 1:1 to JSON Schema. Override inference on individual properties.
Type and format
export type Session = {
/** @openapi.type string @openapi.format uuid */
id: string;
/** @openapi.type integer @openapi.format int64 */
createdAt: number;
/** @openapi.type string @openapi.format date-time */
expiresAt: string;
/** @openapi.type string @openapi.format binary */
avatar?: string;
};
Common format values: uuid, date, date-time, email, uri, int32, int64, float, double, byte, binary.
Constraints
export type WidgetListQueryParams = PaginationOptions & {
/** @openapi.minLength 1 @openapi.maxLength 100 */
name?: string;
/** @openapi.min 1 @openapi.max 100 */
limit?: number;
/** @openapi.pattern ^[a-z0-9-]+$ */
slug?: string;
};
Required vs optional
Optional TypeScript properties (?) export as optional by default. Force required when the API demands a key but your types allow omission during construction:
export type CreateWidgetRequestBody = {
name: string;
/** @openapi.required */
enabled?: boolean;
};
Nullable
export type WidgetAttributes = {
/** @openapi.nullable */
description?: string | null;
};
Examples and defaults
/**
* @openapi.component
* @openapi.example {"name":"Demo","enabled":true}
*/
export type WidgetDraft = {
name: string;
/** @openapi.default true */
enabled: boolean;
};
Enums and unions
String enums
/**
* @openapi.component
* @openapi.enum draft,published,archived
*/
export type WidgetStatus = "draft" | "published" | "archived";
Or annotate a TypeScript enum:
/**
* @openapi.component
*/
export enum WidgetStatus {
Draft = "draft",
Published = "published",
Archived = "archived",
}
oneOf / discriminated unions
/**
* @openapi.component
* @openapi.discriminator type
* @openapi.discriminatorMapping widget:WidgetEntity,user:UserEntity
*/
export type CatalogEntity = WidgetEntity | UserEntity;
/**
* @openapi.component
*/
export type WidgetEntity = {
/** @openapi.enum widget */
type: "widget";
attributes: WidgetAttributes;
};
References
Referencing a type marked @openapi.component emits $ref automatically. This is the default. Use component types wherever a schema is shared.
/**
* @openapi.component
*/
export type PaginationMeta = {
total: number;
limit: number;
offset: number;
};
export type WidgetListResponse = {
data: WidgetEntity[];
meta: PaginationMeta; // → $ref: '#/components/schemas/PaginationMeta'
};
Exported fragment:
properties:
data:
type: array
items:
$ref: '#/components/schemas/WidgetEntity'
meta:
$ref: '#/components/schemas/PaginationMeta'
Inline override
Use @openapi.inline when a shape should be embedded in the spec instead of linked, typically one-off objects that do not belong in components:
export type WidgetListResponse = {
data: WidgetEntity[];
/** @openapi.inline */
meta: PaginationMeta;
};
On a route response generic:
export type GetWidgetRoute = Route<
"get",
IdentifyWidgetRouteFragment,
{},
{},
/** @openapi.inline */
Success<WidgetEntity>
>;
Types without @openapi.component are always inlined; they have no component key to reference. Add @openapi.component first, then opt out with @openapi.inline when needed.
Parameters
Path params come from RouteFragment :param segments. Query and header params come from *QueryParams types and request config.
Path parameters
export type IdentifyWidgetRouteFragment = RouteFragment<
"/widget/:id",
{
/**
* @openapi.description Widget UUID.
* @openapi.type string
* @openapi.format uuid
*/
id: string;
}
>;
Query parameters
export type WidgetListQueryParams = PaginationOptions & {
/**
* @openapi.in query
* @openapi.description Filter by name substring.
*/
name?: string;
/**
* @openapi.in query
* @openapi.style form
* @openapi.explode true
*/
ids?: string[];
};
Renaming parameters
When the API param name differs from the TypeScript property:
export type WidgetListQueryParams = {
/** @openapi.in query @openapi.name sort[createdAt] */
sortCreatedAt?: "asc" | "desc";
};
Reusable parameter components
Define once, reference from multiple routes:
/**
* @openapi.component parameters
* @openapi.in query
*/
export type OffsetParameter = number;
// On a route:
/**
* @openapi.parameter OffsetParameter
* @openapi.parameter LimitParameter
*/
export type ListWidgetRoute = Route<"get", WidgetRouteFragment, WidgetListQueryParams, {}, ...>;
Request bodies
The fourth generic on Route is the request body schema. Add metadata on the body type or the route:
/**
* @openapi.component requestBodies
* @openapi.description Payload for creating a widget.
*/
export type CreateWidgetRequestBody = {
name: string;
enabled: boolean;
};
/**
* @openapi.requestBody CreateWidgetBody
* @openapi.operationId createWidget
* @openapi.summary Create a widget
*/
export type CreateWidgetRoute = Route<
"post",
WidgetRouteFragment,
{},
CreateWidgetRequestBody,
Success<WidgetEntity>
>;
For multipart uploads, annotate file fields:
export type UploadWidgetMediaBody = {
/** @openapi.type string @openapi.format binary */
file: File;
};
Responses
The fifth generic on Route is the primary (usually 200) response. Add more status codes on the route alias:
/**
* @openapi.operationId getWidget
* @openapi.summary Get a widget by ID
* @openapi.tag widget
* @openapi.response 404 NotFoundResponse
* @openapi.response 403 ForbiddenResponse
*/
export type GetWidgetRoute = Route<
"get",
IdentifyWidgetRouteFragment,
{},
{},
Success<WidgetEntity>
>;
Status code override
When success is not 200:
/**
* @openapi.response 201
* @openapi.operationId createWidget
*/
export type CreateWidgetRoute = Route<
"post",
WidgetRouteFragment,
{},
CreateWidgetRequestBody,
Success<WidgetEntity>
>;
Reusable response components
/**
* @openapi.component responses
* @openapi.description Resource not found.
*/
export type NotFoundResponse = {
errors: { status: string; detail: string }[];
};
/**
* @openapi.component responses
*/
export type ForbiddenResponse = {
errors: { status: string; detail: string }[];
};
Reference by name in @openapi.response 404 NotFoundResponse.
Content type
JSON is the default. Override when needed:
/**
* @openapi.response 200
* @openapi.content application/vnd.api+json
*/
export type GetWidgetRoute = Route<...>;
Operations
Metadata on route aliases becomes operation-level OpenAPI fields:
/**
* @openapi.operationId listWidgets
* @openapi.summary List widgets
* @openapi.description Returns a paginated list of widgets the caller can access.
* @openapi.tag widget,catalog
* @openapi.deprecated
*/
export type ListWidgetRoute = Route<
"get",
WidgetRouteFragment,
WidgetListQueryParams,
{},
CollectionSuccess<WidgetEntity>
>;
| Annotation | OpenAPI field |
|---|---|
@openapi.operationId | operationId |
@openapi.summary | summary |
@openapi.description | description |
@openapi.tag a,b | tags (comma-separated) |
@openapi.deprecated | deprecated: true |
If @openapi.operationId is omitted, export derives one from the registry key or method + path.
Security
Define schemes as components, then attach them to routes.
/**
* @openapi.component securitySchemes
* @openapi.scheme bearer
* @openapi.bearerFormat JWT
*/
export type BearerAuth = unknown;
/**
* @openapi.security BearerAuth
*/
export type CreateWidgetRoute = Route<"post", ...>;
/**
* @openapi.security BearerAuth
* @openapi.security ApiKeyAuth
*/
export type DeleteWidgetRoute = Route<"delete", ...>;
Global security defaults live in accorudo.config.ts; route annotations override per operation.
Full example
// models/widget.ts
/**
* @openapi.component
*/
export type WidgetAttributes = {
/** @openapi.type integer @openapi.format int64 */
id: number;
name: string;
/** @openapi.nullable */
description?: string | null;
};
/**
* @openapi.component
*/
export type WidgetEntity = {
/** @openapi.enum widget */
type: "widget";
attributes: WidgetAttributes;
};
// models/common.ts
/**
* @openapi.component responses
*/
export type NotFoundResponse = {
errors: { status: string; detail: string }[];
};
// routes/widget.ts
export type WidgetRouteFragment = RouteFragment<"/widget">;
export type IdentifyWidgetRouteFragment = RouteFragment<
"/widget/:id",
{ /** @openapi.format uuid */ id: string }
>;
export type WidgetListQueryParams = PaginationOptions & {
/** @openapi.description Filter by name. */
name?: string;
};
export type CreateWidgetRequestBody = {
name: string;
enabled: boolean;
};
/**
* @openapi.operationId listWidgets
* @openapi.summary List widgets
* @openapi.tag widget
*/
export type ListWidgetRoute = Route<
"get",
WidgetRouteFragment,
WidgetListQueryParams,
{},
CollectionSuccess<WidgetEntity>
>;
/**
* @openapi.operationId getWidget
* @openapi.summary Get a widget
* @openapi.tag widget
* @openapi.response 404 NotFoundResponse
* @openapi.security BearerAuth
*/
export type GetWidgetRoute = Route<
"get",
IdentifyWidgetRouteFragment,
{},
{},
Success<WidgetEntity>
>;
/**
* @openapi.operationId createWidget
* @openapi.response 201
* @openapi.tag widget
* @openapi.security BearerAuth
*/
export type CreateWidgetRoute = Route<
"post",
WidgetRouteFragment,
{},
CreateWidgetRequestBody,
Success<WidgetEntity>
>;
Export produces $ref-linked components, typed parameters, and multi-status responses, without leaving the Accorudo type system.
Import round-trip
accorudo import emits equivalent @openapi.* comments on generated types. Hand-edited annotations are preserved on re-import when the underlying operation is unchanged. Prefer editing annotations over patching exported YAML directly. The types remain the source of truth.