Skip to main content

Query Parameters

OPRA provides a declarative system for defining, validating, and coercing query string parameters. Parameters are declared on the controller or operation and automatically decoded before your handler runs — no manual req.query parsing needed.


Declaring parameters​

Operation level​

Use .QueryParam() in the decorator chain to declare a parameter for that specific endpoint:

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

@HttpController({ path: 'customers' })
export class CustomersController {
@(HttpOperation.GET()
.QueryParam('status', { type: 'string', required: false })
.QueryParam('minAge', { type: 'integer', required: false })
.Response(200, { type: ArrayType(Customer) }))
async findMany(ctx: HttpContext) {
const { status, minAge } = ctx.queryParams;
// status: string | undefined
// minAge: number | undefined (already coerced from string)
}
}

Controller level​

Parameters declared on the controller are available to every operation in that controller. Useful for parameters that apply across all endpoints — for example a tenant or locale:

@HttpController({ path: 'customers' })
@HttpController.QueryParam('locale', { type: 'string', required: false })
export class CustomersController {
@(HttpOperation.GET()
.Response(200, { type: ArrayType(Customer) }))
async findMany(ctx: HttpContext) {
const { locale } = ctx.queryParams; // available on every operation
}

@(HttpOperation.GET({ path: ':id' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { locale } = ctx.queryParams; // here too
}
}

Options​

OptionTypeDefaultDescription
typestring | Type—OPRA type name ('string', 'integer', 'boolean', …) or a class decorated with @ComplexType(). Controls validation and coercion.
requiredbooleanfalseIf true, OPRA throws a BadRequestError when the parameter is absent.
defaultany—Value used when the parameter is absent. The default is placed in ctx.queryParams as if the client had sent it.
isArraybooleanfalseAccepts repeated values (?tag=a&tag=b) or comma-separated lists and decodes them as an array.
arraySeparatorstring','Delimiter used to split array values within a single query string entry.
deprecatedboolean | string—Marks the parameter deprecated in the API schema. Pass a string for a deprecation message.
parser(v: any) => any—Custom post-processing function applied after type coercion.

Type coercion​

Query string values are always received as strings. OPRA coerces them to the declared type automatically before placing them in ctx.queryParams. No manual conversion is needed.

@(HttpOperation.GET()
.QueryParam('limit', { type: 'integer', default: 20 })
.QueryParam('active', { type: 'boolean' }))
async list(ctx: HttpContext) {
const { limit, active } = ctx.queryParams;
// limit → number (e.g. ?limit=50 → 50)
// active → boolean (e.g. ?active=true → true)
}

Supported primitive type names: 'string', 'integer', 'number', 'bigint', 'boolean', 'date', 'datetime', 'time', 'uuid', 'object'.


Validation​

OPRA validates each parameter against its declared type before calling your handler. If validation fails, the request is rejected with a 400 BadRequestError — your handler is never called.

@(HttpOperation.GET()
.QueryParam('page', { type: 'integer', required: true }))
async list(ctx: HttpContext) {
// Guaranteed: ctx.queryParams.page is an integer.
// ?page=abc → 400 Bad Request (before handler runs)
// no ?page → 400 Bad Request (required)
}

You can use any OPRA-registered type including enum types:

@(HttpOperation.GET()
.QueryParam('order', {
type: EnumType(['asc', 'desc']),
default: 'asc',
}))
async list(ctx: HttpContext) {
const order = ctx.queryParams.order; // 'asc' | 'desc'
// ?order=foo → 400 Bad Request
}

Array parameters​

Declare isArray: true to accept multiple values for the same key:

@(HttpOperation.GET()
.QueryParam('tag', { type: 'string', isArray: true }))
async list(ctx: HttpContext) {
const tags = ctx.queryParams.tag; // string[]
// ?tag=a&tag=b → ['a', 'b']
// ?tag=a,b,c → ['a', 'b', 'c'] (comma-split by default)
}

Change the separator with arraySeparator:

@(HttpOperation.GET()
.QueryParam('ids', {
type: 'integer',
isArray: true,
arraySeparator: '|',
}))
async list(ctx: HttpContext) {
const ids = ctx.queryParams.ids; // number[]
// ?ids=1|2|3 → [1, 2, 3]
}

Default values​

When a parameter has a default, it is placed in ctx.queryParams as if the client sent it — including coercion and validation:

@(HttpOperation.GET()
.QueryParam('limit', { type: 'integer', default: 20 })
.QueryParam('skip', { type: 'integer', default: 0 }))
async list(ctx: HttpContext) {
const { limit, skip } = ctx.queryParams;
// If client sends no limit/skip, these are 20 and 0 respectively.
}

Reading parameters from context​

Decoded parameters are available on ctx.queryParams. Only parameters explicitly declared with .QueryParam() appear here — undeclared keys are ignored.

@(HttpOperation.GET()
.QueryParam('status', { type: 'string' })
.QueryParam('page', { type: 'integer', default: 1 })
.QueryParam('limit', { type: 'integer', default: 20 }))
async list(ctx: HttpContext) {
const { status, page, limit } = ctx.queryParams;
}

To access raw, undeclared query string values, parse ctx.request.url directly:

const url = new URL(ctx.request.originalUrl, 'http://x');
const rawValue = url.searchParams.get('some-undeclared-key');
note

ctx.queryParams only contains keys that were declared on the controller or operation. If a client sends an extra query parameter that is not declared, it is silently ignored — it will not appear in ctx.queryParams and will not cause an error.


Custom post-processing with parser​

For transformations that go beyond type coercion, supply a parser function. It receives the already-coerced value and returns the final value stored in ctx.queryParams:

@(HttpOperation.GET()
.QueryParam('fields', {
type: 'string',
isArray: true,
parser: (fields: string[]) => new Set(fields),
}))
async list(ctx: HttpContext) {
const fields: Set<string> = ctx.queryParams.fields;
}