Marks a type as a Relay Global Object Identification node. Every @node-decorated type gets a synthesised opaque ID encoding (typeId, primary-key columns); @nodeId fields encode and decode that ID. The Query.node(id:) and Query.nodes(ids:) entry points dispatch by typeId prefix to the matching @node type’s table at runtime.
@node only takes effect on types that also carry @table; the global ID embeds primary-key (or unique-key) columns from the bound table.
SDL signature
directive @node(
typeId: String,
keyColumns: [String!]
) on OBJECT
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
|
|
type’s GraphQL name |
The discriminator embedded in the ID that tells |
|
|
primary-key columns |
Ordered column list embedded in the ID. Must correspond to a primary key or another unique key on the bound table. Defaults to the primary key in declaration order. Set explicitly to decouple the ID from primary-key changes. |
Canonical example
The example schema’s Customer, Address, and Film types all opt into Relay Node:
interface Node { id: ID! }
type Customer implements Node @table(name: "customer") @node {
id: ID! @nodeId
customerId: Int! @field(name: "CUSTOMER_ID")
firstName: String! @field(name: "FIRST_NAME")
# ...
}
type Address implements Node @table(name: "address") @node {
id: ID! @nodeId
# ...
}
type Film implements Node @table(name: "film") @node {
id: ID! @nodeId
# ...
}
@node here uses every default: typeId falls back to Customer, Address, Film; keyColumns falls back to each table’s primary key. The corresponding entry points:
type Query {
node(id: ID!): Node
nodes(ids: [ID!]!): [Node]
}
Query.node(id:) decodes the opaque ID, reads the embedded typeId, and dispatches to the matching @node type’s table. Query.nodes(ids:) is the batched form: the rewrite groups IDs by typeId, fans out one batched SELECT per type, and scatters rows back to the request’s original positions.
Composite-PK nodes follow the same shape:
type FilmActor implements Node @table(name: "film_actor") @node {
id: ID! @nodeId
# ...
}
film_actor has a composite (actor_id, film_id) primary key; the synthesised ID embeds both columns in declaration order. @lookupKey on [ID!]! keyed by the same node type produces the corresponding VALUES join over both PK columns.
Set typeId explicitly when the GraphQL type name might change but the ID must remain stable:
type Person implements Node @table(name: "customer") @node(typeId: "C") {
id: ID! @nodeId
}
The typeId: "C" keeps existing IDs valid even if Person is later renamed to User or Customer.
Constraints
-
The decorated type must also carry
@table; the bound table supplies the columns to embed. -
The type must implement the
Nodeinterface (type X implements Node …). Without the interface,Query.node(id:)cannot return the type. -
The type must declare an
id: ID!field decorated with@nodeId. The directive on the type alone does not produce the ID; the field-level marker tells the generator which slot is the ID. -
keyColumnsmust form a primary key or another unique key on the bound table. The build fails if the columns don’t form a unique constraint. -
keyColumnsordering matters and is part of the ID’s wire format; reordering breaks already-issued IDs. -
typeIdcollisions across types are rejected at startup. If two types share atypeId(whether explicit or defaulted),Query.node(id:)cannot dispatch unambiguously.
Editor support
The graphitron LSP completes keyColumns: element values against the columns of the type’s @table-backed jOOQ class (the same column set @field(name:) completes against), flags typos per-element, and hovers each element to surface its GraphQL type. Cursor inside the empty list literal offers every column; once you start typing, the editor narrows to the matching prefix.
See also
-
@nodeIdis the field-level counterpart that encodes and decodes the ID. -
@tablesupplies the columns embedded in the ID. -
@lookupKeyon[ID!]decodes opaque node IDs into typed batch keys (composite-PK NodeId lookups). -
How-to: Global object IDs covers stable-ID strategies,
keyColumnsmigrations, andQuery.node/Query.nodesdispatch.