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 | TypeOPRA 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.
defaultanyValue 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 | stringMarks the parameter deprecated in the API schema. Pass a string for a deprecation message.
parser(v: any) => anyCustom 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;
}