API Document
Overview
ApiDocument is the root object of an OPRA application. It holds three things together:
- A type registry — all named data types available to the API
- A reference map — linked external documents accessible under namespace aliases
- An API definition — the HTTP, MQ, or WebSocket transport configuration
Every field type, every operation parameter, and every response body is ultimately resolved through an ApiDocument. You create one at application startup and pass it to the transport adapter.
Creating a Document
Use ApiDocumentFactory.createDocument() to build an ApiDocument asynchronously. The factory resolves all type references, validates the schema, and returns a fully initialised document.
→ Full API: ApiDocument
import { ApiDocumentFactory } from '@opra/common';
const document = await ApiDocumentFactory.createDocument({
info: {
title: 'Customer API',
version: '1.0',
},
types: [Customer, Address, Gender],
api: {
transport: 'http',
name: 'CustomerApi',
url: '/api',
controllers: [CustomersController],
},
});
createDocument accepts either an init object (shown above) or a URL pointing to a remote OPRA schema JSON.
// Load from a remote schema URL
const document = await ApiDocumentFactory.createDocument('https://api.example.com/opra.json');
Init Options
The init object passed to createDocument mirrors the Schema structure but accepts TypeScript classes and instances directly instead of raw JSON.
| Field | Type | Description |
|---|---|---|
info | DocumentInfo | Human-readable metadata. → See: Schema > Document Info |
types | DataTypeInitSources | Types to register. Accepts a class, array, or record. |
references | Record<string, ReferenceThunk> | Linked external documents keyed by namespace alias. |
api | HttpApi | MQApi | WSApi | API transport configuration. |
info
const document = await ApiDocumentFactory.createDocument({
info: {
title: 'Customer API',
version: '1.0',
description: 'Manages customer records and orders.',
contact: [{ name: 'API Support', email: 'api@example.com' }],
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
},
});
types
Types can be provided as an array of classes or as a record keyed by type name:
// Array form — name is derived from each class
const document = await ApiDocumentFactory.createDocument({
types: [Customer, Address, Gender],
});
// Record form — explicit name override
const document = await ApiDocumentFactory.createDocument({
types: {
Customer,
ShippingAddress: Address,
},
});
Any class decorated with @ComplexType(), @SimpleType(), or registered via EnumType() is accepted.
Registering Types
Types can be registered at multiple levels of the document hierarchy. A type is accessible from the node where it is registered and from all of its descendants — but not from parent or sibling nodes.
ApiDocument → types visible everywhere in the document
└── api → types visible to all controllers and operations
└── HttpController → types visible to all operations in this controller
└── HttpOperation → types visible only within this operation
Root-level types
Types registered at the root are available everywhere in the document. This is the right place for shared domain types used across multiple controllers or operations.
import { ComplexType, ApiField } from '@opra/common';
@ComplexType({ description: 'Customer record' })
class Customer {
@ApiField({ readonly: true }) declare _id?: number;
@ApiField({ required: true }) declare givenName: string;
@ApiField({ required: true }) declare familyName: string;
@ApiField() declare email?: string;
}
@ComplexType()
class Address {
@ApiField({ required: true }) declare city: string;
@ApiField() declare street?: string;
}
const document = await ApiDocumentFactory.createDocument({
// Available to every controller and operation in the document
types: [Customer, Address],
api: { ... },
});
Controller-level types
Types declared on a controller are accessible only within that controller and its operations. Other controllers cannot see them. Use .UseType() on the @HttpController() decorator to register types at controller scope.
import { ComplexType, ApiField, HttpController, HttpOperation } from '@opra/common';
@ComplexType()
class CustomerFilter {
@ApiField() declare givenName?: string;
@ApiField() declare familyName?: string;
@ApiField() declare email?: string;
}
// CustomerFilter is only visible within CustomersController and its operations
@(HttpController({ path: '/customers' })
.UseType(CustomerFilter))
export class CustomersController {
// operations ...
}
// CustomerFilter is NOT accessible here
@HttpController({ path: '/orders' })
export class OrdersController {
// operations ...
}
const document = await ApiDocumentFactory.createDocument({
types: [Customer], // shared — visible everywhere
api: {
transport: 'http',
name: 'CustomerApi',
controllers: [CustomersController, OrdersController],
},
});
Operation-level types
Types declared on an operation are scoped to that single operation — they are not visible to sibling operations or the parent controller. Use .UseType() on the @HttpOperation() decorator.
import { ComplexType, ApiField, HttpController, HttpOperation } from '@opra/common';
@ComplexType()
class SearchResult {
@ApiField() declare items?: Customer[];
@ApiField() declare total?: number;
}
@HttpController({ path: '/customers' })
export class CustomersController {
// SearchResult is only visible within this operation
@(HttpOperation.Get()
.UseType(SearchResult)
.Response(200, { type: SearchResult }))
async search(ctx: HttpContext) {
// ...
}
@HttpOperation.Get('/:id')
async get(ctx: HttpContext) {
// SearchResult is NOT accessible here
}
}
Lookup and scoping rules
When resolving a type name, OPRA walks up the hierarchy from the current node until it finds a match:
- Current node's
types - Parent node's
types - … up to the root document's
types - Built-in types in the
opranamespace
This means a child node can shadow a root-level type by registering a type with the same name — the nearest definition wins.
// Verify registration
document.types.has('Customer'); // true
document.types.has(Customer); // true
Built-in primitive types (string, number, boolean, date, …) are always available — they are registered automatically in the internal opra namespace and do not need to be listed in types.
References & Namespaces
Use references to link external ApiDocument instances into the current document. Each reference is assigned a namespace alias; types from the linked document are then accessible as namespace:TypeName.
import { ApiDocumentFactory } from '@opra/common';
import { CustomerModelsDocument } from './customer-models/index.js';
const document = await ApiDocumentFactory.createDocument({
info: { title: 'Order API', version: '1.0' },
references: {
// 'cm' becomes the namespace alias for the linked document
cm: () => CustomerModelsDocument.create(),
},
api: {
transport: 'http',
name: 'OrderApi',
controllers: [OrdersController],
},
});
Each value in references is a thunk — a zero-argument function returning a Promise<ApiDocument>. Thunks are resolved lazily during createDocument.
Reference types are accessed by prefixing the type name with the namespace:
// Resolves 'Customer' from the 'cm' namespace
const customerType = document.node.getDataType('cm:Customer');
Type Lookup
After creation, resolve types at runtime through document.node:
// Returns undefined if not found
const type = document.node.findDataType('Customer');
// Throws if not found
const type = document.node.getDataType('Customer');
// Typed variants — throw if the type exists but is the wrong kind
const complexType = document.node.getComplexType('Customer');
const simpleType = document.node.getSimpleType('email');
const enumType = document.node.getEnumType('Gender');
const arrayType = document.node.getArrayType('Tags');
All lookup methods accept a string name, a constructor reference, or an enum object:
document.node.getDataType('Customer'); // by name
document.node.getDataType(Customer); // by constructor
document.node.getDataType(Gender); // by enum object
An optional scope argument filters by scope pattern:
// Only resolves if the type is visible in the 'public' scope
const type = document.node.getDataType('Customer', 'public');
Codec Generation
Every DataType exposes generateCodec() to produce a validator function from the valgen library. Call it on a resolved type to get an encoder or decoder.
const customerType = document.node.getComplexType('Customer');
// Decoder — validates and coerces inbound request data
const decode = customerType.generateCodec('decode');
// Encoder — validates and serializes outbound response data
const encode = customerType.generateCodec('encode');
const customer = decode({ givenName: 'Jane', familyName: 'Doe' });
Codec options
| Option | Type | Description |
|---|---|---|
scope | string | Apply field visibility rules for the given scope. |
partial | boolean | 'deep' | Treat all fields as optional. Useful for PATCH operations. |
projection | string[] | '*' | Limit which fields are included in the output. |
ignoreReadonlyFields | boolean | Strip readonly fields from the input (decode only). |
ignoreWriteonlyFields | boolean | Strip writeonly fields from the output (encode only). |
allowPatchOperators | boolean | Accept patch operator expressions (e.g. $set, $unset). |
// Decode a PATCH body — all fields optional, readonly fields stripped
const patchDecode = customerType.generateCodec('decode', {
partial: true,
ignoreReadonlyFields: true,
});
// Encode for the 'public' scope — hides fields restricted to 'db' or 'admin'
const publicEncode = customerType.generateCodec('encode', {
scope: 'public',
ignoreWriteonlyFields: true,
});
Exporting the Schema
Serialize the document to its JSON schema representation with export() or toJSON():
const schema = document.export();
// → OpraSchema.ApiDocument — plain JSON-serializable object
const json = JSON.stringify(document); // calls toJSON() internally
The exported object matches the Schema format exactly and can be written to a file, served over HTTP, or used to initialise a client-side document.
import { writeFileSync } from 'node:fs';
writeFileSync('opra.json', JSON.stringify(document.export(), null, 2));
An optional scope filter limits which types and fields appear in the export:
// Export only what is visible in the 'public' scope
const publicSchema = document.export({ scope: 'public' });