Skip to main content

Http Api

Overview

OPRA's HTTP API provides a decorator-driven layer for building REST endpoints on top of adapters such as Express and NestJS. You define controllers and operations once using TypeScript decorators; the adapter handles routing, content negotiation, and lifecycle hooks. Query parameters, path parameters, headers, cookies, and request bodies are automatically validated and decoded against their declared types before your handler is called, and response payloads are encoded and validated before they are sent to the client.

An HTTP API in OPRA is built from two decorator types:

  • @HttpController() — groups related operations under a shared URL path. Controllers can be nested to model resource hierarchies.
  • @HttpOperation() — defines a single HTTP endpoint on a controller method. Shorthand variants (HttpOperation.GET(), HttpOperation.POST(), …) set the HTTP method automatically.

Both decorators expose chainable methods for parameters, request bodies, responses, and type registration.

HTTP RequestRoute Matchingcontroller · operationParametersdecode & validateRequest Bodydecode & validateHandlerasync methodResponseencode & validateHTTP Responseerror400Bad Requesterror400Bad Requesterror4xx/5xxError Responseerror500Server Error

HttpController

Decorate a class with @HttpController() to register it as an HTTP controller.

import { HttpController } from '@opra/common';

@HttpController({
path: '/customers',
description: 'Customer resource',
})
export class CustomersController {
// operations ...
}

Pass the controller class to ApiDocumentFactory.createDocument() in the api.controllers array (→ ApiDocument):

const document = await ApiDocumentFactory.createDocument({
api: {
transport: 'http',
name: 'CustomerApi',
url: '/api',
controllers: [CustomersController, OrdersController],
},
});

Options

OptionTypeDescription
pathstringURL path segment for this controller. Defaults to the class name without the Controller suffix.
namestringRegistry name. Defaults to the class name without the Controller suffix.
descriptionstringHuman-readable description.
controllersType[]Nested child controllers.

Nested Controllers

Use the controllers option to nest controllers under a parent. The child's path is appended to the parent's path.

import { HttpController } from '@opra/common';

@HttpController({ path: '/notes' })
export class CustomerNotesController {
// /api/customers/:customerId/notes/...
}

@HttpController({
path: '/customers',
controllers: [CustomerNotesController],
})
export class CustomersController {
// /api/customers/...
}

Controller Parameters

Parameters defined on a controller are shared by all its operations. Use chainable methods on the @HttpController() decorator.

import { HttpController } from '@opra/common';

@(HttpController({ path: '/customers/:customerId' })
.PathParam('customerId', Number)
.Header('x-tenant-id', { type: String, required: true })
.QueryParam('locale', String))
export class CustomersController { }
MethodLocationDescription
.QueryParam(name, type?)queryQuery string parameter shared by all operations in this controller.
.PathParam(name, type?)pathURL path parameter. Must be declared in the path as :name.
.Header(name, type?)headerRequest header shared by all operations in this controller.
.Cookie(name, type?)cookieCookie value.
note

Parameters defined on a controller are inherited by all its operations. An operation can reference them directly without redeclaring them. Operation-level parameters with the same name override the controller-level definition for that operation only.

Each method accepts a type as a shorthand second argument, or a full HttpParameter.Options object:

@(HttpController({ path: '/orders' })
.QueryParam('status', {
type: 'string',
required: false,
description: 'Filter orders by status',
deprecated: 'Use state instead',
}))
export class OrdersController { }

HttpOperation

Define an HTTP endpoint on a controller method using HttpOperation shorthand variants or the base decorator with an explicit method option.

import { HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';

@HttpController({ path: '/customers' })
export class CustomersController {

// Base decorator — method passed explicitly
@HttpOperation({ method: 'POST', path: '/:id/activate' })
async activate(ctx: HttpContext) { }

// Shorthand variants — method is preset
@HttpOperation.GET()
async findMany(ctx: HttpContext) { }

@HttpOperation.POST()
async create(ctx: HttpContext) { }

@HttpOperation.GET('/:id')
async get(ctx: HttpContext) { }

@HttpOperation.PATCH('/:id')
async update(ctx: HttpContext) { }

@HttpOperation.DELETE('/:id')
async delete(ctx: HttpContext) { }
}

Shorthand variants

DecoratorMethodDescription
HttpOperation.GET(options?)GETRetrieve a resource or collection.
HttpOperation.POST(options?)POSTCreate a new resource.
HttpOperation.PUT(options?)PUTReplace a resource.
HttpOperation.PATCH(options?)PATCHPartially update a resource.
HttpOperation.DELETE(options?)DELETEDelete a resource.
HttpOperation.HEAD(options?)HEADRetrieve headers only.
HttpOperation.OPTIONS(options?)OPTIONSRetrieve supported methods.
HttpOperation.SEARCH(options?)SEARCHSearch with a request body.

Options

OptionTypeDescription
pathstringURL path segment appended to the controller path.
mergePathbooleanMerge path into parent instead of appending.
descriptionstringHuman-readable description.

Operation Parameters

Parameters defined on an operation override or extend controller-level parameters. The same chainable methods are available.

@HttpController({ path: '/customers' })
export class CustomersController {

@(HttpOperation.GET()
.QueryParam('givenName', String)
.QueryParam('familyName', String)
.QueryParam('limit', { type: Number, default: 20 })
.QueryParam('skip', { type: Number, default: 0 }))
async findMany(ctx: HttpContext) { }

@(HttpOperation.GET('/:id')
.PathParam('id', Number))
async get(ctx: HttpContext) { }

@(HttpOperation.POST('/import')
.Header('x-import-source', { type: String, required: true }))
async import(ctx: HttpContext) { }
}

Request Body

Use .RequestContent() to define the request body type for an operation. The type argument can be any OPRA data type — a ComplexType class, or a type produced by OmitType, PickType, PartialType, or RequiredType.

Data Types

import { HttpController, HttpOperation, OmitType } from '@opra/common';
import { HttpContext } from '@opra/http';

@HttpController({ path: '/customers' })
export class CustomersController {

@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer }))
async create(ctx: HttpContext) { }

@(HttpOperation.PATCH('/:id')
.RequestContent(OmitType(Customer, ['_id']))
.Response(200, { type: Customer }))
async update(ctx: HttpContext) { }
}

Pass a full options object to control content type, size limits, and partial mode:

@(HttpOperation.POST()
.RequestContent({
type: Customer,
contentType: 'application/json',
maxContentSize: 1048576, // 1 MB
})
.Response(201, { type: Customer }))
async create(ctx: HttpContext) { }

Multipart / File Upload

Use .MultipartContent() for multipart/form-data requests. Chain .File() and .Field() on the content scope to declare individual parts.

import { HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';

@HttpController({ path: '/customers' })
export class CustomersController {

@(HttpOperation.POST('/:id/avatar')
.MultipartContent(
{
maxFiles: 1,
maxFileSize: 5242880, // 5 MB
},
content => content
.File('avatar', { required: true })
.Field('altText', { type: String }),
)
.Response(200))
async uploadAvatar(ctx: HttpContext) { }
}
MethodDescription
.File(fieldName, options?)Declares an uploaded file part.
.Field(fieldName, options?)Declares a non-file form field.

Responses

Use .Response() to declare one or more response shapes on an operation. Chain multiple calls for different status codes.

@HttpController({ path: '/customers' })
export class CustomersController {

@(HttpOperation.GET('/:id')
.Response(200, { type: Customer })
.Response(404))
async get(ctx: HttpContext) { }

@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer })
.Response(400, { description: 'Validation error' })
.Response(409, { description: 'Customer already exists' }))
async create(ctx: HttpContext) { }
}

The first argument to .Response() is the HTTP status code — either a number, a range string ('2xx'), or an array of codes. The second argument is an optional options object:

OptionTypeDescription
typestring | TypeResponse body data type.
contentTypestring | string[]MIME type of the response. Defaults to application/json.
descriptionstringHuman-readable description of this response.
partialboolean | 'deep'Response fields may be partially present.

Entity Operations

HttpOperation.Entity provides a set of pre-built operation decorators for standard CRUD endpoints. Each one wires up the HTTP method, path, request body, and response automatically based on the given type.

Available entity operations

DecoratorMethodDescription
HttpOperation.Entity.Create(type, options?)POSTCreate a new resource.
HttpOperation.Entity.Get(type, options?)GETFetch a single resource by key.
HttpOperation.Entity.Replace(type, options?)PUTReplace a resource by key.
HttpOperation.Entity.Update(type, options?)PATCHPartially update a resource by key.
HttpOperation.Entity.Delete(type, options?)DELETEDelete a resource by key.
HttpOperation.Entity.FindMany(type, options?)GETList or search resources.
HttpOperation.Entity.UpdateMany(type, options?)PATCHBulk update resources.
HttpOperation.Entity.DeleteMany(type, options?)DELETEBulk delete resources.

Single-resource operations

import { HttpController, HttpOperation, OmitType } from '@opra/common';
import { HttpContext } from '@opra/http';

@(HttpController({ path: '/customers/:customerId' })
.KeyParam('customerId', Number))
export class CustomersController {

@HttpOperation.Entity.Create(Customer, {
requestBody: { type: OmitType(Customer, ['_id']) },
})
async create(ctx: HttpContext) { }

@HttpOperation.Entity.Get(Customer)
async get(ctx: HttpContext) { }

@HttpOperation.Entity.Update(Customer)
async update(ctx: HttpContext) { }

@HttpOperation.Entity.Delete(Customer)
async delete(ctx: HttpContext) { }
}

Collection operations

FindMany, UpdateMany, and DeleteMany support filter and sort configuration via chainable methods:

@(HttpOperation.Entity.FindMany(Customer)
.Filter('_id', ['=', '!=', '<', '>', '>=', '<=', 'in', '!in'])
.Filter('givenName', ['=', '!=', 'like', '!like', 'ilike', '!ilike'])
.Filter('familyName', ['=', '!=', 'like', '!like'])
.Filter('email', ['=', '!='])
.SortFields('_id', 'givenName', 'familyName')
.DefaultSort('givenName'))
async findMany(ctx: HttpContext) { }

@(HttpOperation.Entity.DeleteMany(Customer)
.Filter('_id', ['=', '!=', 'in', '!in']))
async deleteMany(ctx: HttpContext) { }
MethodApplicable toDescription
.Filter(field, operators)FindMany, UpdateMany, DeleteManyDeclares a filterable field and its allowed comparison operators.
.SortFields(...fields)FindManyDeclares which fields can be used for sorting.
.DefaultSort(field)FindManySets the default sort field when none is specified.