Skip to main content

Path Parameters

Path parameters are segments of the URL that carry variable values — for example the id in /customers/42. OPRA extracts, coerces, and validates them before your handler runs, exactly like query parameters.


Declaring a path parameter

Define the placeholder in the operation's path option using :name syntax, then declare the parameter with .PathParam():

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

@HttpController({ path: 'customers' })
export class CustomersController {
@(HttpOperation.GET({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
// id: number (coerced from the URL string)
}

@(HttpOperation.DELETE({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(204))
async delete(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
}
note

Path parameters are always required. A URL cannot match a route unless every placeholder is filled, so OPRA marks path parameters required by default — you cannot make them optional.


Controller-level path parameters

When a path parameter appears in every operation of a controller (for example a resource key like :id), declare it once on the controller instead of repeating it on each operation:

@HttpController({ path: 'customers/:id' })
@HttpController.PathParam('id', { type: 'integer' })
export class CustomerController {
@(HttpOperation.GET()
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams; // available on every operation
}

@(HttpOperation.PATCH()
.Response(200, { type: Customer }))
async update(ctx: HttpContext) {
const { id } = ctx.pathParams;
}

@(HttpOperation.DELETE()
.Response(204))
async delete(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
}

KeyParam — resource identifier shorthand

@HttpController.KeyParam() is a convenience decorator for the common case of a single primary-key parameter. It does two things at once:

  1. Appends @:name to the controller path (e.g. customerscustomers/:id)
  2. Registers the parameter with keyParam: true so OPRA knows it is the entity identifier
@HttpController({ path: 'customers' })
@HttpController.KeyParam('id', { type: 'integer' })
export class CustomerController {
// Effective path: customers/:id
// ctx.pathParams.id is automatically available as a number

@(HttpOperation.GET()
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
}

@(HttpOperation.PATCH()
.Response(200, { type: Customer }))
async update(ctx: HttpContext) {
const { id } = ctx.pathParams;
}
}

Use KeyParam when the controller represents a single resource addressed by one primary key. Use PathParam for all other cases, including composite keys or nested resources.


Options

OptionTypeDefaultDescription
typestring | TypeOPRA type name ('string', 'integer', 'uuid', …) or a class. Controls coercion and validation.
requiredbooleantrueAlways true for path params — cannot be overridden.
defaultanyNot applicable for path parameters.
deprecatedboolean | stringMarks the parameter deprecated in the API schema.
parser(v: any) => anyCustom post-processing function applied after type coercion.

Type coercion

Path parameter values arrive as strings from the URL. OPRA coerces them to the declared type before calling your handler:

@(HttpOperation.GET({ path: ':id' })
.PathParam('id', { type: 'integer' })
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
// /customers/42 → id === 42 (number)
// /customers/abc → 400 Bad Request (not a valid integer)
}
@(HttpOperation.GET({ path: ':slug' })
.PathParam('slug', { type: 'string' })
.Response(200, { type: Article }))
async getBySlug(ctx: HttpContext) {
const { slug } = ctx.pathParams;
// /articles/hello-world → slug === 'hello-world'
}

Validation

If the value cannot be coerced to the declared type, OPRA rejects the request with a 400 BadRequestError before calling your handler:

@(HttpOperation.GET({ path: ':id' })
.PathParam('id', 'uuid') // shorthand: second arg can be a type name string
.Response(200, { type: Customer }))
async getOne(ctx: HttpContext) {
const { id } = ctx.pathParams;
// /customers/not-a-uuid → 400 Bad Request
// /customers/550e8400-e29b-... → id is the UUID string
}

Multiple path parameters

Declare each segment independently:

@HttpController({ path: 'orgs/:orgId/projects/:projectId/tasks' })
@HttpController.PathParam('orgId', { type: 'integer' })
@HttpController.PathParam('projectId', { type: 'integer' })
export class TasksController {
@(HttpOperation.GET()
.Response(200, { type: ArrayType(Task) }))
async list(ctx: HttpContext) {
const { orgId, projectId } = ctx.pathParams;
// Both are numbers, both were validated before reaching here
}
}

Reading parameters from context

Decoded path parameters are available on ctx.pathParams:

const { id, slug } = ctx.pathParams;

To access the raw, unparsed path segment, read ctx.request.params directly (this is the raw Express params object, before OPRA coercion):

const rawId = ctx.request.params['id']; // always a string