Foreign Keys
Overview
The @Foreign decorator establishes relationships between database tables in your data controller. When a field holds a foreign key (an ID referencing another table), the Data API automatically resolves the reference and includes the related record in API responses. This works for both single references and arrays of references.
Basic Usage
Apply @Foreign to a field that stores a foreign key value. The decorator replaces the raw ID in the response with the full referenced record.
import { Foreign } from "@antelopejs/interface-data-api/metadata";
import {
Field,
Index,
RegisterTable,
Relation,
Table,
} from "@antelopejs/interface-database-decorators";
@RegisterTable("users")
class User extends Table {
@Index({ primary: true })
@Field("string")
declare _id: string;
@Field("string")
declare name: string;
@Field("string")
declare email: string;
}
@RegisterTable("orders")
class Order extends Table {
@Index({ primary: true })
@Field("string")
declare _id: string;
@Field("string")
@Relation({ to: () => User })
declare userId: string;
@Field("number")
declare total: number;
}
@RegisterDataController()
class OrderAPI extends DataController(
Order,
DefaultRoutes.All,
Controller("/orders"),
) {
@ModelReference()
@Model(OrderModel, "my-database")
declare orderModel: OrderModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare _id: string;
@Listable()
@Access(AccessMode.ReadOnly)
@Foreign(User)
declare userId: User;
@Listable()
@Access(AccessMode.ReadOnly)
declare total: number;
}
A GET request to /orders/get?id=order-123 returns the full user object instead of a raw ID:
{
"_id": "order-123",
"userId": {
"_id": "user-456",
"name": "Bob",
"email": "[email protected]"
},
"total": 99.99
}
Decorator Parameters
@Foreign(table, index?, multi?, pluck?, schema?)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
table | Class<Table> or string | Yes | - | The target table class or table name string |
index | string | No | "_id" | The index on the target table to match against |
multi | boolean | No | false | Set to true when the field holds an array of foreign keys |
pluck | string[] | No | All fields | List of fields to include from the referenced record |
schema | string | No | Controller schema | Name of the schema the target table is registered in, when it differs from the controller's schema |
Table Reference Methods
Class Reference
Pass the table class directly. This provides IDE autocompletion, refactoring support, and compile-time validation.
@Foreign(User)
declare userId: User;
String Reference
Pass the registered table name as a string. Use this when importing the class would create circular dependencies.
@Foreign("users")
declare userId: User;
Multi-Record References
When a field stores an array of foreign key IDs, set the multi parameter to true. The API resolves each ID and returns an array of full records.
The table-level @Relation({ many: true }) is declarative metadata recording that the field holds multiple target keys; API-side resolution is controlled solely by the multi parameter of @Foreign.
@RegisterTable("carts")
class Cart extends Table {
@Index({ primary: true })
@Field("string")
declare _id: string;
@Field(["string"])
@Relation({ to: () => CartItem, many: true })
declare items: string[]; // Array of CartItem IDs
}
@RegisterTable("cart_items")
class CartItem extends Table {
@Index({ primary: true })
@Field("string")
declare _id: string;
@Field("string")
declare name: string;
@Field("number")
declare price: number;
}
@RegisterDataController()
class CartAPI extends DataController(
Cart,
DefaultRoutes.All,
Controller("/carts"),
) {
@ModelReference()
@Model(CartModel, "my-database")
declare cartModel: CartModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare _id: string;
@Listable()
@Access(AccessMode.ReadOnly)
@Foreign(CartItem, undefined, true)
declare items: CartItem[];
}
Response:
{
"_id": "cart-123",
"items": [
{ "_id": "item-1", "name": "Widget", "price": 9.99 },
{ "_id": "item-2", "name": "Gadget", "price": 24.99 }
]
}
Selective Field Retrieval with Pluck
The pluck parameter limits which fields are included from the referenced record. This reduces response payload size when you only need specific fields.
@RegisterDataController()
class CartAPI extends DataController(
Cart,
DefaultRoutes.All,
Controller("/carts"),
) {
@ModelReference()
@Model(CartModel, "my-database")
declare cartModel: CartModel;
@Listable()
@Access(AccessMode.ReadOnly)
declare _id: string;
// Only include name and email from User
@Access(AccessMode.ReadOnly)
@Foreign(User, undefined, false, ["name", "email"])
declare userId: User;
// Only include name and price from CartItem
@Access(AccessMode.ReadOnly)
@Foreign(CartItem, undefined, true, ["name", "price"])
declare items: CartItem[];
}
Response without pluck:
{
"_id": "cart-123",
"userId": {
"_id": "user-456",
"name": "Bob",
"email": "[email protected]",
"password": "hashed_value",
"internalNotes": "..."
}
}
Response with pluck: ["name", "email"]:
{
"_id": "cart-123",
"userId": {
"name": "Bob",
"email": "[email protected]"
}
}
Custom Index
Use the index parameter to match against a secondary index instead of the primary key.
// Match userId against the "email" index on the User table
@Foreign(User, "email")
declare userEmail: User;
Cross-Schema References
By default, the target table is resolved in the same schema as the controller's table. Use the schema parameter to reference a table registered in a different schema. At runtime the foreign lookup is resolved against that schema's default instance.
// Resolve userId against the User table registered in the "auth" schema
@Access(AccessMode.ReadOnly)
@Foreign(User, undefined, false, undefined, "auth")
declare userId: User;
When a table class is passed (instead of a string), its schema is inferred from its @RegisterTable metadata, so the schema parameter is only needed to override that or when passing a string table name. If both the class metadata and the explicit schema option specify a schema and they disagree, an error is thrown.
Disable Foreign Key Resolution
Use the noForeign route option to skip foreign key resolution for specific endpoints. This returns raw IDs instead of resolved objects.
const routes = {
get: DefaultRoutes.Get,
getRaw: DefaultRoutes.WithOptions(DefaultRoutes.Get, { noForeign: "true" }),
};
Error Handling
- Missing References - If a referenced record does not exist, the field value is
null. - Invalid IDs - Invalid foreign key values result in
nullfor the resolved field. - Arrays with Missing Items - In multi-record references, missing items appear as
nullin the array.
Next Steps
See the filters documentation to learn how to add filtering capabilities to list endpoints.