11. Relationship endpoints
Relationship methods are part of RestApiPlugin; no extra relationship plugin
is required. Install a storage plugin and an HTTP connector before registering
resources. Configure the connector’s mountPath, for example /api, as in the
quickstart.
There are two kinds of read endpoint:
| Endpoint | Programmatic method | Response |
|---|---|---|
/api/publishers/1/relationships/authors |
getRelationship |
JSON:API identifier linkage |
/api/publishers/1/authors |
getRelated |
Related resource records |
A linkage read returns a document even when programmatic record format is plain.
A related read respects format: plain to-one reads return a record or null,
plain to-many reads return a collection with data, and JSON:API reads return
a document. Related reads apply the target’s fields, filtering and pagination;
they do not promise every related record in a single response.
Set up a runnable example
Insert these blocks into the starting script on a fresh database, before the HTTP server starts.
await api.addResource('publishers', {
schema: { name: { type: 'string', required: true } },
relationships: { authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' } }
})
await api.addResource('authors', {
schema: {
name: { type: 'string', required: true },
publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
}
})
await api.resources.publishers.createKnexTable()
await api.resources.authors.createKnexTable()
const publisher = await api.resources.publishers.post({ data: { name: 'Scribner' } })
const stephen = await api.resources.authors.post({ data: { name: 'Stephen King' } })
const peter = await api.resources.authors.post({ data: { name: 'Peter Straub' } })
The inverse relationships are explicitly declared. A belongsTo field does not
automatically create an arbitrary reverse hasOne or hasMany endpoint.
Add membership and read each representation
await api.resources.publishers.postRelationship({
id: publisher.id,
relationshipName: 'authors',
relationshipData: [
{ type: 'authors', id: stephen.id },
{ type: 'authors', id: peter.id }
]
})
const linkage = await api.resources.publishers.getRelationship({
id: publisher.id, relationshipName: 'authors'
})
const related = await api.resources.publishers.getRelated({
id: publisher.id,
relationshipName: 'authors',
queryParams: { fields: { authors: 'name' }, sort: ['name'] }
})
console.log('Identifiers:', linkage.data)
console.log('Records:', related.data)
Linkage contains { type: 'authors', id } identifiers. Related records contain
IDs and names, ordered Peter then Stephen. Relationship-only writes take
relationshipData, not data; HTTP wraps that linkage in a data member.
POST adds members and DELETE removes specified members; both apply only to
supported to-many relationships. PATCH replaces a to-many set or a to-one target.
All three relationship write methods return undefined.
Replace and remove members
await api.resources.publishers.patchRelationship({
id: publisher.id,
relationshipName: 'authors',
relationshipData: [{ type: 'authors', id: stephen.id }]
})
const replaced = await api.resources.publishers.getRelationship({
id: publisher.id, relationshipName: 'authors'
})
await api.resources.publishers.deleteRelationship({
id: publisher.id,
relationshipName: 'authors',
relationshipData: [{ type: 'authors', id: stephen.id }]
})
const empty = await api.resources.publishers.getRelationship({
id: publisher.id, relationshipName: 'authors'
})
console.log('After replacement:', replaced.data)
console.log('After removal:', empty.data)
Replacement leaves Stephen alone; removal leaves an empty array. Authors remain
in the database. With hasMany, detachment clears the child’s nullable foreign
key. With ordinary many-to-many storage, it removes the pivot row. Required
relationships cannot be cleared by detaching a child.
Change and clear a to-one target
await api.resources.authors.patchRelationship({
id: peter.id,
relationshipName: 'publisher',
relationshipData: { type: 'publishers', id: publisher.id }
})
const peterPublisher = await api.resources.authors.getRelated({
id: peter.id, relationshipName: 'publisher'
})
await api.resources.authors.patchRelationship({
id: peter.id, relationshipName: 'publisher', relationshipData: null
})
const cleared = await api.resources.authors.getRelationship({
id: peter.id, relationshipName: 'publisher'
})
console.log('Related publisher:', peterPublisher.name)
console.log('Cleared linkage:', cleared.data)
The related read returns Scribner; the final linkage is null. Use null only when the relationship permits it. Polymorphic linkage also requires its target type; see polymorphic relationships.
HTTP equivalents
After running all blocks, both authors are unassigned. These commands add them, read both endpoints, and clear the collection again:
curl -X POST http://localhost:3000/api/publishers/1/relationships/authors \
-H 'Content-Type: application/vnd.api+json' \
-d '{"data":[{"type":"authors","id":"1"},{"type":"authors","id":"2"}]}'
curl http://localhost:3000/api/publishers/1/relationships/authors
curl --globoff 'http://localhost:3000/api/publishers/1/authors?fields[authors]=name&sort=name'
curl -X PATCH http://localhost:3000/api/publishers/1/relationships/authors \
-H 'Content-Type: application/vnd.api+json' \
-d '{"data":[]}'
Successful generated relationship writes return HTTP 204 without a body. Use IDs
returned by the server for persistent databases. Resource PATCH is a different
body shape: data.type, data.id, and data.relationships alongside any
attributes being changed.
Permissions and transactions
Relationship operations apply their method-specific permission checks and
resource/target visibility. Linkage discovery is not a bypass for related query
permissions. Ordinary pivot rows also have their own query visibility; canonical
links are separate from pivot resource rows. Configure hooks through
api.customize using the hook guide.
A successful relationship write participates in the library’s transaction
lifecycle. Compose several operations with a handle from
api.transaction. A loop of independent operations
is not a single atomic transaction. Failures report the
write outcome; do not blindly retry an unknown outcome.
Use relationship writes for membership-only changes, related reads for records, and resource PATCH when updating attributes and relationships together. Where provided, follow response links for discovery and pagination; relative links must be resolved against the API URL by clients that require absolute URLs.