HTTP Methods
Every OPRA HTTP operation is associated with an HTTP method. The eight method decorators — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and SEARCH — are called on HttpOperation to declare an operation on a controller method.
Method decorators
| Decorator | HTTP Method | Typical use |
|---|---|---|
HttpOperation.GET(options?) | GET | Fetch a resource or collection |
HttpOperation.POST(options?) | POST | Create a resource |
HttpOperation.PUT(options?) | PUT | Replace a resource entirely |
HttpOperation.PATCH(options?) | PATCH | Apply a partial update |
HttpOperation.DELETE(options?) | DELETE | Remove a resource |
HttpOperation.HEAD(options?) | HEAD | Fetch headers only |
HttpOperation.OPTIONS(options?) | OPTIONS | Describe allowed methods |
HttpOperation.SEARCH(options?) | SEARCH | Complex query via request body |
All eight accept the same optional options object and return an HttpOperationDecorator — a chainable builder that also acts as a TypeScript decorator.
Basic usage
Without chaining, apply the decorator directly to the method:
import { HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';
@HttpController({ path: 'customers' })
export class CustomersController {
@HttpOperation.GET({ path: ':id', description: 'Returns a customer by id' })
async getOne(ctx: HttpContext) {}
@HttpOperation.DELETE({ path: ':id' })
async delete(ctx: HttpContext) {}
}
Chaining
Every method decorator returns an HttpOperationDecorator with chainable methods for declaring parameters, request body, and responses. When chaining, wrap the entire expression in parentheses so TypeScript applies the result as a decorator:
@(HttpOperation.GET({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
@(HttpOperation.GET()
.QueryParam('status', { type: 'string' })
.QueryParam('limit', { type: 'integer', default: 20 })
.Response(200, { type: ArrayType(Customer) }))
async list(ctx: HttpContext) {
const { status, limit } = ctx.queryParams;
}
@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer }))
async create(ctx: HttpContext) {
const body = await ctx.getBody<Customer>();
}
Chainable methods
| Method | Description |
|---|---|
.QueryParam(name, options?) | Declare a query string parameter |
.PathParam(name, options?) | Declare a path segment parameter |
.Header(name, options?) | Declare a request header parameter |
.Cookie(name, options?) | Declare a cookie parameter |
.RequestContent(type, options?) | Declare a typed request body content |
.MultipartContent(fields, options?) | Declare a multipart request body |
.Response(statusCode, options?) | Declare a response shape |
.UseType(type) | Register an additional type in the operation's type map |
Options
| Option | Type | Description |
|---|---|---|
path | string | URL path relative to the controller. Use :name syntax for path parameters (e.g. ':id', 'archive/:year/:month'). |
mergePath | boolean | When true, concatenate with the parent path without a separator (useful for query suffixes). |
description | string | Human-readable description of the operation, included in the generated schema. |
immediateFetch | boolean | Read and buffer the request body immediately before calling the handler. Use when the body is always needed. |
allowPatchOperators | boolean | Accept _$push / _$pull patch operator fields in the decoded body. |
allowNullOptionals | boolean | Allow optional body fields to be null in addition to absent. Useful for PATCH endpoints where null means "clear this field". |
requestBody | HttpRequestBody.Options | Pre-configure the request body (size limits, content, etc.) without using .RequestContent(). |
Declaring response type
Use .Response() to declare what the operation returns. This registers the response in the schema and tells OPRA clients what to expect:
@(HttpOperation.GET({ path: ':id' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {}
@(HttpOperation.GET()
.Response(200, { type: ArrayType(Customer) }))
async list(ctx: HttpContext) {}
@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer })
.Response(409, { description: 'Conflict — duplicate key' }))
async create(ctx: HttpContext) {}
For operations that return a collection, wrap the type with ArrayType(). Without it, the schema describes a single object even if your handler returns an array.
PATCH — partial updates
PATCH endpoints typically accept partial bodies. Use allowNullOptionals when clients need to send null to explicitly clear a field:
@(HttpOperation.PATCH({ path: ':id' })
.PathParam('id', { type: 'integer' })
.RequestContent('application/json', { partial: true, allowNullOptionals: true })
.Response(200, { type: Customer }))
async update(ctx: HttpContext) {
const body = await ctx.getBody<Partial<Customer>>();
// body.email may be undefined (omitted), a string, or null (clear it)
}
SEARCH — query via request body
SEARCH is an HTTP method extension for complex queries that are too large for a query string. It is treated like GET semantically but accepts a request body:
@(HttpOperation.SEARCH()
.RequestContent(CustomerFilter)
.Response(200, { type: ArrayType(Customer) }))
async search(ctx: HttpContext) {
const filter = await ctx.getBody<CustomerFilter>();
}
Full example
import { ArrayType, EnumType, HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';
@HttpController({ path: 'customers' })
@HttpController.QueryParam('locale', { type: 'string', required: false })
export class CustomersController {
@(HttpOperation.GET()
.QueryParam('status', {
type: EnumType(['active', 'inactive']),
required: false,
})
.QueryParam('limit', { type: 'integer', default: 20 })
.QueryParam('skip', { type: 'integer', default: 0 })
.Response(200, { type: ArrayType(Customer) }))
async list(ctx: HttpContext) {
const { status, limit, skip, locale } = ctx.queryParams;
}
@(HttpOperation.GET({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer }))
async create(ctx: HttpContext) {
const body = await ctx.getBody<Customer>();
}
@(HttpOperation.PATCH({ path: ':id' })
.PathParam('id', { type: 'integer' })
.RequestContent('application/json', { partial: true, allowNullOptionals: true })
.Response(200, { type: Customer }))
async update(ctx: HttpContext) {
const body = await ctx.getBody<Partial<Customer>>();
}
@(HttpOperation.DELETE({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(204))
async delete(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
}