The @Listable decorator controls which fields are included in list endpoint responses. By default, list operations only return fields explicitly marked as listable, keeping payloads lightweight. The decorator supports multiple pluck modes for different levels of detail, and list responses include built-in pagination metadata.
The @Sortable decorator marks fields that can be used as a sorting key in list queries.
Fields decorated with @Listable() are included in list responses. Fields without this decorator are excluded from list results but still appear in single-record get responses (provided they have read access).
import {
Access,
AccessMode,
Listable,
ModelReference,
} from "@antelopejs/interface-data-api/metadata";
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare _id: string;
@Listable()
@Access(AccessMode.ReadOnly)
declare email: string;
@Access(AccessMode.ReadOnly)
declare biography: string; // Excluded from list, included in get
}A request to GET /users/list returns _id and email for each record. A request to GET /users/get?id=user-123 returns all three fields.
Pluck modes allow a single controller to serve multiple list views with different field selections. Each @Listable call can target a named mode.
Calling @Listable() without a mode name registers the field under the default "list" mode.
@Listable()
declare name: string;Pass true and a mode name to register a field under a specific pluck mode.
@Listable() // Included in default "list" mode
@Listable(true, "detailed") // Also included in "detailed" mode
declare name: string;
@Listable(true, "detailed") // Only in "detailed" mode
declare internalNotes: string;
@Listable() // Only in default "list" mode
declare email: string;Pass false to explicitly exclude a field from a mode.
@Listable()
@Listable(false, "minimal") // Excluded from "minimal" mode
declare description: string;Define separate routes for each pluck mode using DefaultRoutes.WithOptions with the pluckMode option.
const routes = {
list: DefaultRoutes.List,
detailed: DefaultRoutes.WithOptions(DefaultRoutes.List, {
pluckMode: "detailed",
}),
};
@RegisterDataController()
class UserAPI extends DataController(User, routes, Controller("/users")) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare name: string;
@Listable(true, "detailed")
@Access(AccessMode.ReadOnly)
declare internalNotes: string;
}GET /users/listreturns onlyname.GET /users/detailedreturns bothnameandinternalNotes.
When a field uses a getter (computed property), the @Listable decorator must specify which database fields the getter depends on. This ensures those fields are fetched from the database.
@Listable(["firstName", "lastName"])
@Access(AccessMode.ReadOnly)
get fullName() {
return `${this.firstName} ${this.lastName}`;
}List responses include pagination metadata. The response structure is:
{
results: T[]; // Records for the current page
total: number; // Total number of matching records
offset: number; // Number of records skipped
limit: number; // Maximum records per page
}| Parameter | Default | Description |
|---|---|---|
limit |
10 |
Maximum number of records to return |
offset |
0 |
Number of records to skip |
GET /users/list?limit=2&offset=4{
"results": [
{ "_id": "user-5", "name": "Eve" },
{ "_id": "user-6", "name": "Frank" }
],
"total": 100,
"offset": 4,
"limit": 2
}Use the maxPage route option to cap the limit parameter. If a request specifies a limit larger than maxPage, the value is clamped to maxPage.
const routes = {
list: DefaultRoutes.WithOptions(DefaultRoutes.List, { maxPage: 25 }),
};
@RegisterDataController()
class UserAPI extends DataController(User, routes, Controller("/users")) {
// A request with ?limit=100 returns at most 25 records
}The @Sortable decorator marks a field as eligible for use as a sorting key in list operations.
import { Sortable } from "@antelopejs/interface-data-api/metadata";
@RegisterDataController()
class UserAPI extends DataController(
User,
DefaultRoutes.All,
Controller("/users"),
) {
@ModelReference()
@Model(UserModel, "my-database")
declare userModel: UserModel;
@Listable()
@Access(AccessMode.ReadOnly)
@Sortable()
declare _id: string;
@Listable()
@Access(AccessMode.ReadOnly)
@Sortable({ noIndex: true })
declare name: string;
@Listable()
@Access(AccessMode.ReadOnly)
@Sortable({ noIndex: true })
declare age: number;
}| Parameter | Description |
|---|---|
sortKey |
The field name to sort by. Must be decorated with @Sortable. |
sortDirection |
"asc" (ascending) or "desc" (descending). Defaults to "asc". |
By default, @Sortable() assumes the field has a database index and uses indexed ordering. Set noIndex: true when the field is not indexed, so the framework performs an in-memory sort instead.
@Sortable() // Uses database index for sorting
declare _id: string;
@Sortable({ noIndex: true }) // In-memory sort, no index required
declare name: string;GET /users/list?sortKey=age&sortDirection=desc&limit=3{
"results": [
{ "_id": "user-1", "name": "Senior", "age": 65 },
{ "_id": "user-2", "name": "Adult", "age": 30 },
{ "_id": "user-3", "name": "Young", "age": 18 }
],
"total": 3,
"offset": 0,
"limit": 3
}If sortKey refers to a field that is not marked with @Sortable, the API returns a 400 Bad Request error.
See the foreign keys documentation to learn how to establish relationships between tables.