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()
@Listable(true, "detailed")
@Access(AccessMode.ReadOnly)
declare name: string;
@Listable(true, "detailed")
@Access(AccessMode.ReadOnly)
declare internalNotes: string;
}GET /users/listreturns onlyname.GET /users/detailedreturns bothnameandinternalNotes.
Modes are independent field sets: "detailed" does not inherit "list". The example includes name in both modes explicitly. A registered list route whose pluckMode has never been declared returns 400 Bad Request unless noPluck is enabled; an unregistered URL instead returns 404.
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() marks the field for indexed ordering. Set noIndex: true when it is not indexed. Both paths build an AQL orderBy operation; this option changes where sorting is staged, not whether rows are sorted in application memory. The provider determines how the database executes that ordering.
@Sortable() // Uses database index for sorting
declare _id: string;
@Sortable({ noIndex: true }) // Non-indexed query ordering
declare name: string;A @Localized field is sorted on its value in the request's locale, the one the controller's @ModifierKey(LocalizationModifier) field holds. A row with no value in that locale, or an empty one, sorts on its fallbackLocale value. This ordering is computed per row, so it never uses an index, whatever noIndex says.
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.