[data-reveal]{opacity:1!important;transform:none!important}
Read and transform request data
API

Read and transform request data

A parameter decorator supplies an argument to your handler from the request. Start with one source per argument, then add a transformation or validation step when the handler needs something other than the raw value.

For a quick example, jump to @Parameter. The custom provider and modifier APIs are for reusable integrations, not a prerequisite for reading request data.

Parameter sources

The API extracts parameters from the following sources:

  • Route parameters - Segments of the URL path marked with :paramName
  • Query parameters - Values from the URL query string
  • HTTP headers - Request header values
  • Request body - Data submitted in the request body
  • Request context - The complete request context object

Parameter decorators

@Parameter

The @Parameter decorator extracts a single value from route parameters, query strings, or headers.

import { Controller, Get, Parameter } from "@antelopejs/interface-api";

class UsersController extends Controller("/users") {
  @Get(":id")
  async getUser(@Parameter("id") id: string) {
    return { id, name: "Example User" };
  }

  @Get("")
  async listUsers(@Parameter("page", "query") page: string | true | undefined) {
    // For a request to /users?page=2, page is "2"
    return { users: ["user1", "user2"], page };
  }

  @Get(":userId/posts/:postId")
  async getUserPost(
    @Parameter("userId") userId: string,
    @Parameter("postId") postId: string,
  ) {
    return { userId, postId };
  }
}

The second argument specifies the source:

SourceDescriptionExample
"param"Route parameters (default)/users/:id extracts id
"query"URL query parameters/users?page=2 extracts page
"header"HTTP request headersExtracts Authorization header value

Missing values produce undefined. For a query parameter, ?page and ?page= produce true, not an empty string; ?page=2 produces "2". Repeated query values use the first value. Account for these cases before converting a value to a number or using it as a string.

@MultiParameter

The @MultiParameter decorator extracts multiple values for a parameter as an array. This is useful for query parameters or headers that appear more than once.

import { Controller, Get, MultiParameter } from "@antelopejs/interface-api";

class ProductsController extends Controller("/products") {
  @Get("search")
  async searchProducts(@MultiParameter("tag") tags: string[]) {
    // For /products/search?tag=js&tag=api, tags is ["js", "api"]
    return { tags, results: ["product1", "product2"] };
  }
}

The @MultiParameter decorator accepts a source argument of "query" (default) or "header".

Missing values produce []. Query values retain empty strings, so ?tag=&tag=api produces ["", "api"]. Header values follow Node.js's parsed header representation; this decorator does not split comma-separated strings.

@RawBody

The @RawBody decorator provides the raw HTTP request body as a Buffer.

import { Controller, Post, RawBody } from "@antelopejs/interface-api";

class PostsController extends Controller("/posts") {
  @Post()
  async createPost(@RawBody() postData: Buffer) {
    const parsed = JSON.parse(postData.toString());
    return { id: "new-post-id", ...parsed };
  }
}

@JSONBody

@JSONBody parses JSON, which can produce an object, an array, a primitive, or null; an empty body produces undefined. It does not validate a schema. Treat external bodies as unknown until validated, and see body limits before accepting uploads.

import { Controller, Post, JSONBody } from "@antelopejs/interface-api";

class UsersController extends Controller("/users") {
  @Post()
  async createUser(@JSONBody() userData: { name: string; email: string }) {
    return { id: "new-user-id", ...userData };
  }
}

@Context

The @Context decorator provides access to the complete RequestContext object.

import {
  Controller,
  Get,
  Context,
  RequestContext,
} from "@antelopejs/interface-api";

class InfoController extends Controller("/info") {
  @Get()
  handleRequest(@Context() ctx: RequestContext) {
    const clientIP = ctx.rawRequest.socket.remoteAddress;
    const requestUrl = ctx.url.toString();
    return { ip: clientIP, url: requestUrl };
  }
}

@Result

The @Result decorator provides access to the HTTPResult response object. This is typically used in postfix handlers to modify the response.

import {
  Controller,
  Get,
  Postfix,
  Result,
  HTTPResult,
} from "@antelopejs/interface-api";

class ResponseController extends Controller("/api") {
  @Get("data")
  async getData() {
    return { data: "example" };
  }

  @Postfix("get", "data")
  async addMetadata(@Result() result: HTTPResult) {
    result.addHeader("X-Timestamp", new Date().toISOString());
  }
}

@WriteStream

The @WriteStream decorator provides a writable stream for sending data to the client. This is useful for streaming responses or server-sent events.

import {
  Controller,
  Get,
  WriteStream,
  Context,
  RequestContext,
} from "@antelopejs/interface-api";
import { PassThrough } from "node:stream";

class StreamController extends Controller("/stream") {
  @Get()
  async streamData(
    @Context() context: RequestContext,
    @WriteStream("text/event-stream") stream: PassThrough,
  ) {
    stream.write("data: Hello\n\n");

    setTimeout(() => {
      stream.write("data: World\n\n");
      stream.end();
    }, 1000);

    return context.response;
  }
}

@Connection

The @Connection decorator provides access to the WebSocket connection object. It can only be used with @WebsocketHandler.

import {
  Controller,
  WebsocketHandler,
  Connection,
} from "@antelopejs/interface-api";

class ChatController extends Controller("/chat") {
  @WebsocketHandler("")
  handleChat(@Connection() connection: any) {
    connection.on("message", (data: string) => {
      connection.send("Received: " + data);
    });
  }
}

Use decorators on class properties

Both @Parameter and @MultiParameter (as well as @RawBody, @JSONBody, @Context, and @Connection) can be applied to class properties. The property values are automatically populated from the request for each handler invocation.

import {
  Controller,
  Get,
  Parameter,
  MultiParameter,
} from "@antelopejs/interface-api";

class ConfigurableController extends Controller("/config") {
  @Parameter("apiKey", "header")
  private apiKey!: string;

  @MultiParameter("tags", "query")
  private tags!: string[];

  @Get()
  async getConfig() {
    return {
      authenticated: Boolean(this.apiKey),
      appliedTags: this.tags,
    };
  }
}

Combine multiple parameters

Handler methods can accept multiple decorated parameters from different sources.

import {
  Controller,
  Put,
  Parameter,
  JSONBody,
} from "@antelopejs/interface-api";

class ArticlesController extends Controller("/articles") {
  @Put(":id")
  async updateArticle(
    @Parameter("id") id: string,
    @JSONBody() data: { title: string; content: string },
  ) {
    return { id, ...data, updated: true };
  }
}

Note: Each parameter should have exactly one provider decorator. Do not apply @Parameter to a parameter that already has @RawBody, @JSONBody, or another provider.

Parameter providers and modifiers

The API uses a system of providers and modifiers to process parameters.

Parameter providers

A parameter provider extracts the initial value from the request. The SetParameterProvider function registers a custom provider.

import {
  SetParameterProvider,
  ReadBody,
  RequestContext,
} from "@antelopejs/interface-api";
import { MakeParameterDecorator } from "@antelopejs/interface-core/decorators";

const RawBody = MakeParameterDecorator((target, key, param) =>
  SetParameterProvider(target, key, param, (context: RequestContext) => {
    return ReadBody(context);
  }),
);

Parameter modifiers

A parameter modifier transforms the value after the provider runs. Unlike providers, you can chain multiple modifiers on a single parameter. The AddParameterModifier function registers a modifier.

import {
  AddParameterModifier,
  RequestContext,
} from "@antelopejs/interface-api";
import { MakeParameterDecorator } from "@antelopejs/interface-core/decorators";

const EnsureNumber = MakeParameterDecorator((target, key, param) =>
  AddParameterModifier(
    target,
    key,
    param,
    (context: RequestContext, value: unknown) => {
      const num = Number(value);
      return isNaN(num) ? 0 : num;
    },
  ),
);

Use custom modifiers alongside built-in decorators:

import { Controller, Get, Parameter } from "@antelopejs/interface-api";

class ExampleController extends Controller("/example") {
  @Get(":id")
  async getExample(@Parameter("id") @EnsureNumber() id: number) {
    return { id, doubled: id * 2 };
  }
}

The @Transform decorator

@Transform applies a function after the provider. This example accepts positive decimal IDs and rejects malformed values with HTTP 400, rather than silently turning 12abc into 12.

import {
  Controller,
  Get,
  HTTPResult,
  Parameter,
  Transform,
  RequestContext,
} from "@antelopejs/interface-api";

function parseId(_context: RequestContext, value: unknown): number {
  if (typeof value !== "string" || !/^[1-9]\d*$/.test(value)) {
    throw new HTTPResult(400, "Expected a positive integer ID");
  }
  const id = Number(value);
  if (!Number.isSafeInteger(id)) {
    throw new HTTPResult(400, "ID is too large");
  }
  return id;
}

class ItemController extends Controller("/items") {
  @Get(":id")
  getItem(@Parameter("id") @Transform(parseId) id: number) {
    return { id };
  }
}

Processing chain

Parameters follow a processing chain from extraction to injection:

Provider => [Modifier 1] => [Modifier 2] => ... => Parameter/Property

The provider runs first to extract the raw value, then each modifier transforms the value in sequence before the final result is passed to the handler. Each result is awaited. Modifiers run in registration order; with stacked legacy decorators, that is bottom to top. A thrown error stops the chain before the handler runs.