Skip to main content
Adopt

IPA-113: Singleton Resources

An API may define singleton resources. A singleton resource must always exist by virtue of the existence of its parent, with one and exactly one per parent.

Guidance

  1. Singleton resources must not have a user-provided or system-generated ID

    paths:
    /groups/{groupId}/settings:
    get:
    operationId: getGroupSettings
    Why:

    The singleton settings is addressed entirely through its parent groupId. There is exactly one per group, so no separate identifier is needed or exposed.

    paths:
    /groups/{groupId}/settings/{settingsId}:
    get:
    operationId: getGroupSettings
    Why:

    A settingsId path parameter implies multiple settings per group and a way to enumerate them. A singleton has exactly one instance per parent, so an identifier has nothing to distinguish.

  2. Singleton resources must not define the Create or Delete standard methods

    • The singleton is implicitly created or deleted when its parent is created or deleted
    paths:
    /groups/{groupId}/settings:
    get:
    operationId: getGroupSettings
    patch:
    operationId: updateGroupSettings
    Why:

    The singleton exposes only Get and Update. Its lifecycle is tied to the parent group: the settings come into existence when the group is created and are removed when the group is deleted, so explicit Create and Delete methods would be meaningless.

    paths:
    /groups/{groupId}/settings:
    post:
    operationId: createGroupSettings
    delete:
    operationId: deleteGroupSettings
    Why:

    Create and Delete imply the singleton can be brought into or out of existence independently of its parent. A singleton must always exist by virtue of its parent, so there is no valid state in which it can be created or deleted on its own.

  3. Singleton resources must define the Get method

    paths:
    /groups/{groupId}/settings:
    get:
    operationId: getGroupSettings
    responses:
    "200":
    description: The group settings.
    Why:

    The singleton defines Get, so consumers can always read the current state of the resource that the parent guarantees exists.

  4. Singleton resources should define the Update method, unless the resource is read-only

    paths:
    /groups/{groupId}/settings:
    get:
    operationId: getGroupSettings
    patch:
    operationId: updateGroupSettings
    Why:

    The mutable singleton exposes Update alongside Get, so consumers can change the settings without recreating the parent. Read-only singletons are the exception and intentionally omit Update.

    1. Identify each singleton resource: a resource addressed through its parent with no identifier of its own.

    2. Determine whether the singleton is intended to be read-only — modifiable only by the server, never by API consumers.

    3. For a singleton that is not read-only, confirm it defines an Update method. Flag any mutable singleton that omits Update.

  5. Singleton resources may define custom methods as appropriate

Example

GET /groups/${groupId}/settings
###
PATCH /groups/${groupId}/settings

Read-Only Singleton Resources

Read-only singleton resources are singleton resources that cannot be modified by API consumers.

  1. Read-only singleton resources must have only the Get method

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    responses:
    "200":
    description: The group billing status.
    Why:

    The read-only singleton billingStatus exposes only Get. It is computed and owned by the server, so consumers can read it but never mutate it.

  2. Read-only singleton resources must not have Create, Update, or Delete methods

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    Why:

    Only Get is present. Create and Delete are already disallowed for all singletons, and a read-only singleton additionally omits Update, so no method can mutate it.

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    patch:
    operationId: updateGroupBillingStatus
    Why:

    The patch is an Update method on a read-only singleton. By definition a read-only singleton cannot be modified by consumers, so exposing Update contradicts the resource's contract.

  3. Read-only singleton resources may have custom methods as appropriate, provided they do not modify the resource

  4. All response schema properties for read-only singleton resources must be marked as read-only

    • In OpenAPI, this means all properties must have readOnly: true
    • All fields in read-only singleton resources are server-owned. For guidance on server-owned fields, see IPA-111
    components:
    schemas:
    GroupBillingStatus:
    type: object
    properties:
    state:
    type: string
    readOnly: true
    lastChargedDate:
    type: string
    format: date-time
    readOnly: true
    Why:

    Every property carries readOnly: true, signalling that all fields are server-owned and cannot be set by consumers. This matches the read-only contract of the singleton.

    components:
    schemas:
    GroupBillingStatus:
    type: object
    properties:
    state:
    type: string
    readOnly: true
    lastChargedDate:
    type: string
    format: date-time
    Why:

    lastChargedDate is missing readOnly: true, so generated clients treat it as writable. On a read-only singleton no field is consumer-writable, so leaving any property unmarked misrepresents the contract.

    1. Identify the response schema for each read-only singleton resource.

    2. Enumerate every property in that schema, including nested object properties.

    3. Flag any property that does not have readOnly: true.

  5. Unsupported operations on read-only singleton resources should return 405 Not Allowed

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    responses:
    "200":
    description: The group billing status.
    Why:

    The read-only singleton documents only get. The unsupported patch is kept out of the spec, and a PATCH /groups/{groupId}/billingStatus call resolves to 405 Not Allowed at the server — the method is recognized but not permitted, rather than 404 (resource missing) or 403 (forbidden). The behavior lives in the implementation while the contract stays clean.

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    patch:
    operationId: updateGroupBillingStatus
    responses:
    "404":
    description: Billing status not found.
    Why:

    Documenting patch and returning 404 muddles two different things: the resource doesn't exist, versus the method isn't allowed. The singleton always exists by virtue of its parent, and the method is recognized but disallowed, so the honest response is 405 — and the method should not be documented at all.

    1. Identify each read-only singleton resource: only Get is documented, with no Create, Update, or Delete.

    2. In the implementation behind that path (routing or controller code), check what an unsupported mutation — POST, PUT, PATCH, or DELETE — returns.

    3. Flag any unsupported mutation that resolves to anything other than 405 Not Allowed (for example 404, 403, or a success code).

  6. Unsupported operations must not be documented

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    responses:
    "200":
    description: The group billing status.
    Why:

    The read-only singleton lists only the operation it actually serves: getGroupBillingStatus. There are no mutation operations in the spec, so doc generators and SDK builders produce only methods the API honors. No generated call targets an endpoint the server will reject.

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    delete:
    operationId: deleteGroupBillingStatus
    responses:
    "405":
    description: Method not allowed.
    Why:

    Even though the delete advertises 405, documenting it surfaces a method the API does not support. Doc generators and SDK builders will emit a deleteGroupBillingStatus call that can never succeed.

Resetting Singleton Resources

Singleton resources can be restored to their default state without deleting the parent resource. For such cases, a :reset custom method must be used.

  1. The :reset custom method name must be reserved exclusively for restoring singleton resources to their default state

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    responses:
    "200":
    description: The group settings restored to default.
    Why:

    The :reset method does exactly what its reserved name implies: it restores the singleton settings to their default state. The name is not reused for any other behavior.

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    description: Recalculates derived metrics for the group settings.
    Why:

    Here :reset is used to recalculate metrics rather than to restore default state. Overloading the reserved name with unrelated behavior makes the method's contract ambiguous across the API.

    1. Locate every custom method named :reset across the spec.

    2. Confirm each one restores a singleton resource to its default state, as described in its summary, description, and behavior.

    3. Flag any :reset method that performs something other than restoring default state.

  2. The :reset custom method must use the POST HTTP method

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    Why:

    The :reset custom method is defined under post, matching the required HTTP method for reset.

    paths:
    /groups/{groupId}/settings:reset:
    put:
    operationId: resetGroupSettings
    Why:

    The :reset method is defined under put rather than post. Reset custom methods must use POST.

  3. The :reset custom method must be idempotent

    • Calling reset multiple times must produce the same result as calling it once
    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    responses:
    "200":
    description: The group settings restored to their default state.
    Why:

    Reset restores the singleton to a fixed default state. Calling it once or many times leaves the resource in the same default state, so the operation is idempotent.

    1. Identify each :reset custom method on a singleton resource.

    2. In the implementation, confirm that reset sets the resource to a fixed default state rather than applying a relative or incremental change.

    3. Verify that invoking reset repeatedly leaves the resource in the same state as a single invocation. Flag any reset whose repeated calls produce differing results.

  4. The :reset custom method must return a 200 OK response with the reset resource in the response body

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    responses:
    "200":
    description: The group settings restored to their default state.
    content:
    application/json:
    schema:
    $ref: "#/components/schemas/GroupSettings"
    Why:

    Reset responds with 200 OK and returns the reset GroupSettings in the response body, so consumers immediately see the restored default state.

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    responses:
    "204":
    description: Settings reset.
    Why:

    A 204 No Content returns no body, so consumers cannot see the restored state without a follow-up Get. Reset must return 200 OK with the reset resource in the body.

  5. The :reset custom method must not have a request body

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    responses:
    "200":
    description: The group settings restored to their default state.
    Why:

    Reset takes no input — it always restores the same default state — so the operation defines no requestBody.

    paths:
    /groups/{groupId}/settings:reset:
    post:
    operationId: resetGroupSettings
    requestBody:
    content:
    application/json:
    schema:
    $ref: "#/components/schemas/GroupSettings"
    responses:
    "200":
    description: The group settings restored to their default state.
    Why:

    A requestBody implies reset accepts caller-supplied values. Reset restores a fixed default state and must take no input, so it must not define a request body.

  6. Read-only singleton resources must not define a :reset custom method

    • Read-only singleton resources cannot be modified, so reset is not applicable
    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    Why:

    The read-only singleton exposes only Get and defines no :reset method. Since the resource cannot be modified, there is no default state to restore.

    paths:
    /groups/{groupId}/billingStatus:
    get:
    operationId: getGroupBillingStatus
    /groups/{groupId}/billingStatus:reset:
    post:
    operationId: resetGroupBillingStatus
    Why:

    Defining :reset on a read-only singleton contradicts its contract: reset modifies the resource by restoring default state, but a read-only singleton cannot be modified by consumers at all.