Skip to main content
Adopt

IPA-103: Methods

An API is composed of one or more methods, which represent a specific operation that a service can perform on behalf of the consumer.

Guidance

  1. API authors should choose from the defined categories in the following order:

    • Standard methods (on collections and resources)
    • Custom methods (on collections, resources, or stateless)
    paths:
    /orders/{orderId}:
    delete:
    operationId: deleteOrder
    Why:

    Cancelling an order is removal of a resource, so the standard delete method covers it. A standard method is the first category that fits, so it is the one used.

    paths:
    /orders/{orderId}:cancel:
    post:
    operationId: cancelOrder
    Why:

    A custom :cancel method is introduced for an operation that a standard delete already expresses. The custom method is chosen ahead of the standard one, reversing the intended order.

    1. Enumerate every operation under paths and pair it with the resource path it acts on.

    2. For each operation, determine the action it performs: create, read, list, update, delete, or something else.

    3. When the action is a plain create/read/list/update/delete on a collection or resource, confirm it is expressed as the matching standard method rather than a custom method.

    4. Report any custom method whose behavior a standard method on the same resource would already cover.

  2. Standard methods must not cause side effects.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    responses:
    "200":
    description: The updated project.
    Why:

    The update method changes only the project named in the path. No other resource is mutated as a consequence, so the standard method stays free of side effects.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    description: >-
    Updates the project and also deactivates every team that belongs to it.
    Why:

    Updating one project mutates a different set of resources, the teams. A standard method that reaches beyond its target hides the second effect from anyone reading the contract.

    1. List the standard methods in the spec (create, read, list, update, delete on collections and resources).

    2. For each one, identify the target resource named in the path.

    3. Inspect the implementation or the documented behavior to find every resource the operation mutates, not only the target.

    4. Report any standard method that mutates a resource other than its target, since that is a side effect.

  3. In scenarios where a side effect is necessary, a custom method should be used.

    paths:
    /projects/{projectId}:archive:
    post:
    operationId: archiveProject
    description: >-
    Archives the project and disables its teams.
    Why:

    The operation deliberately touches more than its target, so it is expressed as a custom method. The custom verb signals up front that the call carries an effect a plain update would not.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    description: >-
    Updates the project and disables its teams.
    Why:

    The same effect is folded into a standard update. The required side effect is hidden behind a method that callers expect to be effect-free.

    1. Start from the operations flagged as carrying a necessary side effect by IPA-103-must-not-cause-side-effects.

    2. For each, check whether it is modeled as a standard method or a custom method.

    3. Report any that carry a deliberate side effect yet are still expressed as a standard method.

  4. A side effect specifically includes mutating client-owned fields of any resource other than the target of the request. The dependent mutation must be performed through the dependent resource's own endpoint.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    /teams/{teamId}:
    patch:
    operationId: updateTeam
    Why:

    Changing a team's client-owned fields goes through the team's own update endpoint. Each resource owns the writes to its own fields.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    description: >-
    Also rewrites the name field on each team in the project.
    Why:

    The project update writes client-owned fields on teams. Those writes belong to the team endpoint, so routing them through the project endpoint hides the mutation from anyone working with the team resource.

    1. For each operation, identify the target resource named in its path.

    2. From the implementation or documented behavior, list the client-owned fields the operation writes on any resource.

    3. Flag any operation that writes client-owned fields on a resource other than its target.

    4. Confirm that an endpoint owned by the dependent resource exists and that the mutation is routed through it instead.

  5. If the operation is fundamentally multi-resource by design, it must be modeled as a custom method.

    paths:
    /projects/{projectId}:transfer:
    post:
    operationId: transferProject
    description: >-
    Moves the project to another owner and reassigns its teams.
    Why:

    The operation touches several resources by design, so it is a custom method. The custom verb makes the multi-resource scope explicit.

    paths:
    /projects/{projectId}:
    patch:
    operationId: updateProject
    description: >-
    Moves the project to another owner and reassigns its teams.
    Why:

    A genuinely multi-resource operation is squeezed into a standard update. The standard method advertises a single-resource change while doing much more.

    1. For each operation, determine from the implementation or documented behavior how many distinct resources it mutates.

    2. Identify operations whose purpose inherently spans more than one resource.

    3. Report any such operation that is modeled as a standard method rather than a custom method.

  6. Side effects limited to read-only or effective values of another resource may occur in standard methods.

  7. Standard methods must guarantee atomicity.

    paths:
    /orders/{orderId}:
    patch:
    operationId: updateOrder
    description: >-
    Applies all field changes together; a failure leaves the order
    unchanged.
    Why:

    The update either lands in full or not at all. A partial result is never observable, which is what atomicity guarantees.

    paths:
    /orders/{orderId}:
    patch:
    operationId: updateOrder
    description: >-
    Updates each field in turn; a mid-way failure leaves some fields
    changed.
    Why:

    A failure partway through leaves the order in a mixed state. The standard method exposes a half-applied result, which atomicity forbids.

    1. List the standard methods that mutate state (create, update, delete).

    2. For each, inspect the implementation or documented behavior to see whether a partial failure can leave the resource half-changed.

    3. Report any mutating standard method that can produce an observable partial result. When atomicity cannot be guaranteed, the operation belongs in a sub-resource, a singleton resource, or a custom method instead.

If a standard method is unsuitable, then custom methods offer a lesser, but still valuable level of consistency, helping the user reason about the scope of the action and the object whose configuration is read to inform that action.

Selecting a custom method may be valuable for:

  • State management of a resource since they usually carry side effects
  • If atomic modifications are required when adding or removing from repeated fields

Response bodies

  1. Every endpoint must support a versioned JSON content type (e.g. application/vnd.atlas.YYYY-MM-DD+json), per IPA-900.

    paths:
    /orders/{orderId}:
    get:
    operationId: getOrder
    responses:
    "200":
    content:
    application/vnd.example.2023-01-01+json:
    schema:
    $ref: "#/components/schemas/Order"
    Why:

    The response is offered under a dated, versioned JSON media type, so the payload shape can evolve under a new date without breaking existing callers.

    paths:
    /orders/{orderId}:
    get:
    operationId: getOrder
    responses:
    "200":
    content:
    application/json:
    schema:
    $ref: "#/components/schemas/Order"
    Why:

    The response uses a bare, unversioned JSON media type. There is no version handle, so a later change to the shape cannot be rolled out without breaking callers pinned to the old shape.

    1. For each operation under paths, collect the media type keys declared under every response's content.

    2. For each operation, check that at least one media type is a versioned JSON type carrying a version token and ending in +json.

    3. Report any operation whose responses offer no versioned JSON media type.

  2. Additional content types (e.g. +csv) may be offered alongside JSON.

  3. When a response body is returned as a JSON content type, it must be a JSON object with a fixed set of named properties at the root. Top-level arrays, primitives, or objects with dynamic (unknown) keys at the root are prohibited because they cannot be consistently typed by schema-based clients and tooling.

    components:
    schemas:
    OrderList:
    type: object
    properties:
    results:
    type: array
    items:
    $ref: "#/components/schemas/Order"
    totalCount:
    type: integer
    Why:

    The root is an object with named, declared properties. A schema-based client can generate one stable type for the response.

    components:
    schemas:
    OrderList:
    type: array
    items:
    $ref: "#/components/schemas/Order"
    Why:

    The root is a bare array. A top-level array leaves no place to add fields later and cannot be typed as a named object, so it is prohibited at the root.

    1. For each operation under paths, find the schema of every JSON response body, following any $ref.

    2. Confirm the root schema declares type: object with a named properties map.

    3. Flag any root schema that is an array, a primitive, or an object that declares only additionalProperties with no named properties.

  4. Collections must be wrapped in an envelope object per IPA-110 (e.g. {"results": [...], "links": [...], "totalCount": 42}).

    paths:
    /orders:
    get:
    operationId: listOrders
    responses:
    "200":
    content:
    application/json:
    schema:
    type: object
    properties:
    results:
    type: array
    items:
    $ref: "#/components/schemas/Order"
    totalCount:
    type: integer
    Why:

    The list response wraps the array in an envelope object that also carries pagination fields. The array sits under a named property, so the envelope can grow without changing the root type.

    paths:
    /orders:
    get:
    operationId: listOrders
    responses:
    "200":
    content:
    application/json:
    schema:
    type: array
    items:
    $ref: "#/components/schemas/Order"
    Why:

    The list response returns the array directly. With no envelope there is nowhere to carry pagination links or a total count, and the root cannot be extended later.

    1. Identify the operations that return a collection — typically list operations on a collection path.

    2. For each, inspect the JSON response schema and confirm the array is held under a named property of a root object rather than returned at the root.

    3. Confirm the envelope carries the pagination fields required by IPA-110.

    4. Report any collection response whose array is returned directly at the root.

Naming

The method name is the Operation ID (operationId) in the OpenAPI Specification.

  1. Operation IDs must be unique.

    paths:
    /orders:
    get:
    operationId: listOrders
    /orders/{orderId}:
    get:
    operationId: getOrder
    Why:

    Each operation carries a distinct operationId, so generated SDKs produce one method per operation with no name collisions.

    paths:
    /orders:
    get:
    operationId: getOrders
    /orders/{orderId}:
    get:
    operationId: getOrders
    Why:

    Two operations share getOrders. A duplicate id makes the mapping from operation to generated method ambiguous.

    1. Collect the operationId of every operation under paths.

    2. Compare the values and find any that appear more than once.

    3. Report each repeated operationId along with the operations that share it.

  2. Operation IDs must be in camelCase.

    paths:
    /orders/{orderId}:
    get:
    operationId: getOrder
    Why:

    The operationId is camelCase: a lowercase first letter and no separators. SDK generators map it straight to an idiomatic method name.

    paths:
    /orders/{orderId}:
    get:
    operationId: Get_Order
    Why:

    The operationId uses an uppercase first letter and an underscore separator. It is neither camelCase nor a clean basis for a generated method name.

    1. For each operation under paths, read its operationId.

    2. Check that the value starts with a lowercase letter and contains only letters and digits, with no spaces, underscores, or hyphens.

    3. Report any operationId that is not camelCase.