Names a GraphQL slot as a Relay Global Object Identification ID. The site axis decides direction: on a FIELD_DEFINITION the directive encodes the parent’s primary-key columns into the opaque ID; on INPUT_FIELD_DEFINITION and ARGUMENT_DEFINITION it decodes the ID back to typed key columns at the carrier. Pairs with @node, which configures the type that owns the ID.
SDL signature
directive @nodeId(
typeName: String
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
|
|
inferred |
The |
Canonical example
Producing IDs on a @node type’s id slot (encode side):
type Customer implements Node @table(name: "customer") @node {
id: ID! @nodeId
customerId: Int! @field(name: "CUSTOMER_ID")
# ...
}
type Address implements Node @table(name: "address") @node {
id: ID! @nodeId
# ...
}
@nodeId here has no typeName: because case (a) applies: the containing type is a @node, so the inference is unambiguous. The selected id is base64-encoded (typeId, primary-key columns).
Cross-type reference (encode side, with explicit type):
type Customer implements Node @table(name: "customer") @node {
addressNodeId: ID @nodeId(typeName: "Address")
@reference(path: [{key: "customer_address_id_fkey"}])
}
addressNodeId produces an Address ID from the FK on customer.address_id. The @reference path points at the address row; @nodeId(typeName: "Address") encodes that row’s PK columns under the Address typeId. (FK-mirror collapse means no extra JOIN: customer.address_id == address.address_id.)
Decoding into typed keys at an argument or input field (decode side):
type Query {
# Same-table @nodeId on an argument: decodes opaque IDs into film primary keys
# and feeds a (film_id) IN (...) lookup. @lookupKey is implicit on same-table
# @nodeId arguments.
filmsByNodeIdArg(ids: [ID!]! @nodeId(typeName: "Film")): [Film!]!
# Composite-PK NodeId @lookupKey: each opaque ID decodes to (actor_id, film_id),
# joined via VALUES(idx, actor_id, film_id) on both PK columns.
filmActorByNodeId(id: [ID!]! @lookupKey): [FilmActor!]!
}
input FilmSameTableNodeIdInput {
filmIds: [ID!] @nodeId(typeName: "Film")
}
On the decode side the rewrite verifies the ID’s embedded typeId against the declared typeName:; mismatched IDs surface as GraphqlErrorException via the ThrowOnMismatch contract before any SQL runs.
Constraints
-
The named or inferred type must carry
@node. The build fails whentypeName:resolves to a non-node type. -
typeName:is required when neither inference rule fires: when the field’s type is not the containing type and no@referenceprovides an unambiguous target table. -
@nodeIddoes not stand alone on a non-@nodefield as the type’s identity. Use it on theid: ID!field of a@nodetype, or on slots that reference a node from elsewhere. -
On
[ID!]arguments wheretypeNamematches the surrounding query’s return-type table,@lookupKeyis implied; an explicit@lookupKeyis permitted but redundant. -
Decode-time ID/typeId mismatch fails the request with a GraphQL error; same with malformed base64.
-
@nodeIdis a binding directive, not a projection. The slot’s GraphQL type must beID!,ID,[ID!], or[ID!]!(lists are valid for batch decode).
Editor support
The graphitron LSP completes typeName: against the set of types that declare @node in the schema; typing into the empty quoted value offers every node-bearing type, and a value that does not resolve to one surfaces as an error inline. Hover on a resolved typeName: shows the type’s typeId and key-column list, pulling each column’s GraphQL type from @field / @table metadata.
See also
-
@nodeconfigures the owning type (typeId,keyColumns). -
@referencesupplies the JOIN path when an ID is encoded from a different row than the parent. -
@lookupKeyis the dispatch path for[ID!]-keyed argument lookups; same-table NodeId arguments imply it. -
How-to: Global object IDs covers cross-type IDs, decode-error handling, and same-table-nodeid filter inputs.