OpenAPI

OpenAPI annotations

JSDoc flags and fields that map Accorudo types to OpenAPI components, parameters, and responses.

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

AnnotationApplies toOpenAPI target
@openapi.componenttypecomponents.schemas
@openapi.component parameterstypecomponents.parameters
@openapi.component responsestypecomponents.responses
@openapi.component requestBodiestypecomponents.requestBodies
@openapi.component securitySchemestypecomponents.securitySchemes
@openapi.typepropertyJSON Schema type
@openapi.formatpropertyJSON Schema format
@openapi.descriptiontype or propertydescription
@openapi.exampletype or propertyexample
@openapi.nullablepropertynullable: true
@openapi.requiredpropertyadd to required
@openapi.inlineproperty or genericinline schema instead of $ref
@openapi.inpath/query param fieldparameter in
@openapi.response CODEroute aliasadditional response
@openapi.operationIdroute aliasoperationId
@openapi.tagroute aliasoperation tags
@openapi.securityroute aliasoperation security
@openapi.deprecatedroute alias or propertydeprecated: 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>
>;
AnnotationOpenAPI field
@openapi.operationIdoperationId
@openapi.summarysummary
@openapi.descriptiondescription
@openapi.tag a,btags (comma-separated)
@openapi.deprecateddeprecated: 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.

Copyright © 2026