diff --git a/ipa/general/0101.mdx b/ipa/general/0101.mdx index dfa5c95..7033239 100644 --- a/ipa/general/0101.mdx +++ b/ipa/general/0101.mdx @@ -11,7 +11,7 @@ high-level design principles: - The fundamental building blocks of an API are [individually named resources](0102.mdx) (nouns) and the relationships and hierarchy that exist between them -- A small number of [standard methods](0103.md) (verbs) provide the semantics +- A small number of [standard methods](0103.mdx) (verbs) provide the semantics for most common operations - [Custom methods](0109.md) are available in situations where the standard methods do not fit. @@ -26,7 +26,7 @@ When designing an API, consider the following (roughly in logical order): - The relationships and hierarchies between those resources - The schema of each resource - The methods (verbs) each resource provides rely as much as possible on the - [standard verbs](0103.md) + [standard verbs](0103.mdx) ### Resources diff --git a/ipa/general/0103.md b/ipa/general/0103.md deleted file mode 100644 index a81a4fb..0000000 --- a/ipa/general/0103.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -id: 103 -state: 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 - -- 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) -- Standard methods **must not** cause side effects - - In such scenarios where a side effect is necessary, a custom method - **should** be used - - A side effect specifically includes mutating - [client-owned fields](0111.md#single-owner-fields) of any resource other - than the target of the request. The dependent mutation **must** be performed - through the dependent resource's own endpoint; if the operation is - fundamentally multi-resource by design, it **must** be modeled as a custom - method per the rule above. - - Side effects limited to read-only or - [effective values](0111.md#effective-values) of another resource **may** - occur in standard methods. -- Standard methods **must** guarantee - [atomicity](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29) - - In cases where atomicity cannot be guaranteed, consider in the following - order: - - A new sub-resource with the appropriate methods - - A [singleton-resource](0113.md) for the corresponding section - - [Custom methods](0109.md), for example, an `Add` or `Remove` operation for - repeated fields - -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 - -- Every endpoint **must** support a versioned JSON content type (e.g. - `application/vnd.atlas.YYYY-MM-DD+json`), per [IPA-900](../sdks/0900.md). - Additional content types (e.g. `+csv`) **may** be offered alongside JSON. -- 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. - - Collections **must** be wrapped in an envelope object per [IPA-110](0110.md) - (e.g. `{"results": [...], "links": [...], "totalCount": 42}`). - -### Naming - -- The method name is the - [Operation ID](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operationid) - (`operationId`) in the OpenAPI Specification -- Operation IDs **must** be unique -- Operation IDs **must** be in `camelCase` diff --git a/ipa/general/0103.mdx b/ipa/general/0103.mdx new file mode 100644 index 0000000..b8fbd6d --- /dev/null +++ b/ipa/general/0103.mdx @@ -0,0 +1,945 @@ +--- +id: 103 +state: 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 + +- 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) +- Standard methods **must not** cause side effects + - In such scenarios where a side effect is necessary, a custom method + **should** be used + - A side effect specifically includes mutating + [client-owned fields](0111.md#single-owner-fields) of any resource other + than the target of the request. The dependent mutation **must** be performed + through the dependent resource's own endpoint; if the operation is + fundamentally multi-resource by design, it **must** be modeled as a custom + method per the rule above. + - Side effects limited to read-only or + [effective values](0111.md#effective-values) of another resource **may** + occur in standard methods. +- Standard methods **must** guarantee + [atomicity](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29) + - In cases where atomicity cannot be guaranteed, consider in the following + order: + - A new sub-resource with the appropriate methods + - A [singleton-resource](0113.md) for the corresponding section + - [Custom methods](0109.md), for example, an `Add` or `Remove` operation for + repeated fields + +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 + +- Every endpoint **must** support a versioned JSON content type (e.g. + `application/vnd.atlas.YYYY-MM-DD+json`), per [IPA-900](../sdks/0900.md). + Additional content types (e.g. `+csv`) **may** be offered alongside JSON. +- 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. + - Collections **must** be wrapped in an envelope object per [IPA-110](0110.md) + (e.g. `{"results": [...], "links": [...], "totalCount": 42}`). + +### Naming + +- The method name is the + [Operation ID](https://swagger.io/docs/specification/v3_0/paths-and-operations/#operationid) + (`operationId`) in the OpenAPI Specification +- Operation IDs **must** be unique +- Operation IDs **must** be in `camelCase` + + + + + +API authors **should** choose a method category in priority order: prefer a +standard method (on a collection or resource), and only fall back to a custom +method when no standard method fits. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: listClusters + post: + summary: Create One Cluster + operationId: createCluster + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + patch: + summary: Update One Cluster + operationId: updateCluster + delete: + summary: Remove One Cluster + operationId: deleteCluster +``` + + + Reading and mutating cluster configuration maps cleanly onto the standard + list/create/get/update/delete methods, so no custom method is introduced. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}:read: + get: + summary: Read One Cluster + operationId: readCluster + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}:set: + post: + summary: Set One Cluster Configuration + operationId: setCluster +``` + + + Plain reads and full-configuration writes are exactly what the standard Get + and Update methods express. Reaching for custom methods first discards the + consistency a standard method would have provided. + + + + + + + Enumerate the operations defined for the resource under review. + + + For each operation, determine whether it expresses a standard method (List, + Get, Create, Update, Delete) or a custom method (a `:verb` suffix or + otherwise non-standard action). + + + For every custom method, ask whether a standard method could express the + same intent without a side effect or atomicity problem. + + + Flag any custom method whose behavior a standard method could have covered; + otherwise confirm the custom method is justified. + + + + + + + +Standard methods (List, Get, Create, Update, Delete) **must not** cause side +effects beyond the resource state they nominally read or write. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + description: >- + Returns the configuration and state of the specified cluster. This + method does not modify any resource. + responses: + "200": + description: OK +``` + + + The Get method only reads cluster state. A standard method that leaves the + system unchanged keeps its semantics predictable for callers. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + description: >- + Returns the specified cluster and triggers a fresh backup snapshot as a + side effect of reading it. + responses: + "200": + description: OK +``` + + + Triggering a backup is a side effect that callers do not expect from a Get. + Side-effecting behavior belongs in a custom method, not a standard one. + + + + + + + Identify each standard method (List, Get, Create, Update, Delete) on the + resource under review. + + + Read the operation summary and description to determine what state the + method is expected to read or write. + + + Check whether the method performs additional actions beyond that nominal + read or write (e.g., triggering jobs, mutating other resources, sending + notifications). + + + Flag any standard method that carries a side effect, and recommend a custom + method for the side-effecting behavior. + + + + + + + +When a side effect is necessary, a custom method **should** be used to carry it +rather than attaching the behavior to a standard method. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}:restart: + post: + summary: Restart One Cluster + operationId: restartCluster + description: >- + Restarts the specified cluster. The restart is modeled as a custom + method because it produces a side effect on the running cluster. + responses: + "202": + description: Accepted +``` + + + The side-effecting restart lives in a custom `:restart` method, keeping the + standard Get/Update methods free of surprising behavior. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Updates cluster fields and, if the instance size changes, restarts the + cluster as part of the same Update call. + responses: + "200": + description: OK +``` + + + Folding a restart into the standard Update method hides a side effect inside a + method callers expect to be side-effect free. A dedicated custom method makes + the action explicit. + + + + + + + For any operation flagged as having a side effect, confirm the side effect + is genuinely required by the use case. + + + Check whether the side effect is expressed through a custom method (e.g., a + `:verb` operation) rather than a standard method. + + + If the side effect is attached to a standard method, recommend extracting it + into a custom method. + + + Confirm the resulting custom method names the action and the object it acts + on. + + + + + + + +Mutating the [client-owned fields](0111.md#single-owner-fields) of any resource +other than the target of the request is a side effect. Such a dependent mutation +**must** be performed through the dependent resource's own endpoint rather than +as a hidden effect of a standard method on the target resource. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Updates fields owned by the target cluster only. Changing the backup + policy is performed separately through the backup policy's own endpoint. + responses: + "200": + description: OK + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/backupSchedule: + patch: + summary: Update One Backup Schedule + operationId: updateBackupSchedule + description: >- + Mutates the client-owned fields of the backup schedule resource through + its own endpoint. + responses: + "200": + description: OK +``` + + + Each resource's client-owned fields are mutated through its own endpoint, so + callers can see and reason about every change rather than discovering an + implicit cross-resource write. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Updates the cluster and, as a side effect, rewrites the client-owned + retention fields of the associated backup schedule resource. + responses: + "200": + description: OK +``` + + + Updating the cluster silently mutates client-owned fields of a different + resource (the backup schedule). That dependent mutation must go through the + backup schedule's own endpoint. + + + + + + + For each standard method, identify the target resource of the request. + + + Determine whether the operation writes any client-owned field belonging to a + resource other than that target. + + + If it does, confirm an endpoint exists on the dependent resource that owns + those fields and that the mutation is performed there instead. + + + Flag any standard method that mutates another resource's client-owned fields + inline, and recommend routing the change through the dependent resource's + own endpoint. + + + + + + + +If an operation is fundamentally multi-resource by design — it must mutate more +than one resource as a single logical action — it **must** be modeled as a +custom method rather than as a standard method on one of the resources. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}:migrate: + post: + summary: Migrate One Cluster + operationId: migrateCluster + description: >- + Moves a cluster to a new project, updating both the source and + destination project resources as a single multi-resource action. + responses: + "202": + description: Accepted +``` + + + An action that inherently spans multiple resources is expressed as an explicit + custom `:migrate` method, so callers understand its cross-resource scope. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Updates the cluster and, when the project field changes, also mutates + both the source and destination project resources in the same call. + responses: + "200": + description: OK +``` + + + A standard Update is being used for a fundamentally multi-resource migration. + An operation that mutates several resources as one logical action belongs in a + custom method. + + + + + + + For each operation, determine the full set of resources whose state it + mutates as part of one logical action. + + + If more than one resource is mutated and the mutations cannot be split into + independent per-resource endpoints, the operation is fundamentally + multi-resource. + + + Confirm any fundamentally multi-resource operation is expressed as a custom + method (e.g., a `:verb` operation) rather than a standard method. + + + Flag any standard method that performs a fundamentally multi-resource action + and recommend remodeling it as a custom method. + + + + + + + +Side effects limited to read-only or +[effective values](0111.md#effective-values) of another resource **may** occur +in standard methods, since they do not mutate any client-owned field of the +other resource. + + + + + +Standard methods **must** guarantee +[atomicity](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29): the +operation either fully succeeds or leaves the resource unchanged, with no +partially applied state. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Applies all changes in the request as a single atomic update. If any + field fails validation, no changes are persisted. + responses: + "200": + description: OK +``` + + + An all-or-nothing Update never leaves the cluster in a half-modified state, so + callers can retry safely and reason about the resource deterministically. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + patch: + summary: Update One Cluster + operationId: updateCluster + description: >- + Applies each field in the request one at a time. If a later field fails, + earlier fields remain persisted. + responses: + "200": + description: OK +``` + + + A partially applied Update leaves the resource in an inconsistent state that + callers cannot reason about or cleanly retry. When atomicity is impossible, + model the operation as a sub-resource, singleton resource, or custom method + instead. + + + + + + + Identify each standard method (Create, Update, Delete) that mutates resource + state. + + + Determine, from the operation description and request schema, whether the + method applies multiple changes that could partially succeed. + + + Confirm the operation is documented and designed to either fully succeed or + leave the resource unchanged. + + + If atomicity cannot be guaranteed, recommend a sub-resource with its own + methods, a singleton resource, or a custom method (such as `Add`/`Remove` + for repeated fields). + + + + + + + +Selecting a custom method **may** be valuable for state management of a resource +(which usually carries side effects) or when atomic modifications are required +when adding to or removing from repeated fields. + + + + + +Every endpoint **must** support a versioned JSON content type (e.g. +`application/vnd.atlas.YYYY-MM-DD+json`), per [IPA-900](../sdks/0900.md). + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + responses: + "200": + description: OK + content: + application/vnd.atlas.2024-08-05+json: + schema: + $ref: "#/components/schemas/Cluster" +``` + + + The operation returns a dated, versioned JSON content type, so SDK generators + and clients can pin to a stable, evolvable representation. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Cluster" +``` + + + Only an unversioned `application/json` content type is offered. Without a + versioned JSON media type, clients cannot pin to a specific API version as + IPA-900 requires. + + + + + + + For each operation, inspect the `content` map of every response body. + + + Check whether at least one media type is a versioned JSON content type + matching `application/vnd.atlas.YYYY-MM-DD+json`. + + + Flag any endpoint whose response bodies do not offer a versioned JSON + content type. + + + + + + + +Additional content types (e.g. a `+csv` media type) **may** be offered alongside +the required versioned JSON content type. + + + + + +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. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster + responses: + "200": + description: OK + content: + application/vnd.atlas.2024-08-05+json: + schema: + type: object + properties: + id: + type: string + name: + type: string + stateName: + type: string +``` + + + The root schema is an object with a fixed set of named properties, so + schema-based clients can generate a stable, strongly typed model. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: listClusters + responses: + "200": + description: OK + content: + application/vnd.atlas.2024-08-05+json: + schema: + type: array + items: + $ref: "#/components/schemas/Cluster" +``` + + + The root of the JSON response is a top-level array. A root array cannot carry + named envelope properties and cannot be consistently typed, so it is + prohibited. + + + + + + + For each operation, find every JSON response body and resolve its root + schema. + + + Confirm the root schema is `type: object` with a fixed set of named + properties. + + + Flag any root schema that is an array, a primitive, or an object whose keys + are dynamic (e.g., declared only through `additionalProperties` or + `patternProperties` with no fixed named properties). + + + + + + + +Collections **must** be wrapped in an envelope object per [IPA-110](0110.md) +(e.g. `{"results": [...], "links": [...], "totalCount": 42}`) rather than +returned as a bare top-level array. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: listClusters + responses: + "200": + description: OK + content: + application/vnd.atlas.2024-08-05+json: + schema: + type: object + properties: + results: + type: array + items: + $ref: "#/components/schemas/Cluster" + links: + type: array + items: + $ref: "#/components/schemas/Link" + totalCount: + type: integer +``` + + + The collection is wrapped in an envelope object with `results`, `links`, and + `totalCount`, matching the IPA-110 pagination envelope and keeping the root a + named object. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: listClusters + responses: + "200": + description: OK + content: + application/vnd.atlas.2024-08-05+json: + schema: + type: array + items: + $ref: "#/components/schemas/Cluster" +``` + + + The collection is returned as a bare top-level array with no envelope, leaving + no place for pagination links or counts and violating the IPA-110 envelope + requirement. + + + + + + + Identify each operation that returns a collection of resources (typically a + List method). + + + Resolve the root schema of its JSON response body. + + + Confirm the collection is wrapped in an envelope object (with the IPA-110 + `results`/`links`/`totalCount` properties) rather than returned as a bare + array. + + + Flag any collection response whose root is an unwrapped array. + + + + + + + +Operation IDs (`operationId`), which serve as the method name, **must** be +unique across the specification. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: listClusters + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster +``` + + + Each operation has a distinct `operationId`, so generated SDK method names and + tooling references resolve unambiguously to a single operation. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters: + get: + summary: Return All Clusters + operationId: getClusters + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getClusters +``` + + + Two operations share the `operationId` `getClusters`, which collides in + generated clients and makes the method name ambiguous. + + + + + + + Collect the `operationId` of every operation across all paths in the + specification. + + + Compare the collected values and detect any duplicates. + + + Flag every `operationId` that appears on more than one operation. + + + + + + + +Operation IDs (`operationId`) **must** be written in `camelCase`. + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: getCluster +``` + + + `getCluster` is camelCase: a lowercase first word with each subsequent word + capitalized, matching the convention SDK generators expect. + + + + + + +```yaml +paths: + /api/atlas/v2/groups/{groupId}/clusters/{clusterName}: + get: + summary: Return One Cluster + operationId: Get_Cluster +``` + + + `Get_Cluster` uses a leading capital and an underscore separator, which is not + camelCase and produces inconsistent generated method names. + + + + + + + Collect the `operationId` of every operation across all paths. + + + For each `operationId`, verify it starts with a lowercase letter and + contains only letters and digits, with word boundaries marked by + capitalization (no underscores, hyphens, or spaces). + + + Flag any `operationId` that does not match camelCase. + + + + + + diff --git a/ipa/general/0104.md b/ipa/general/0104.md index 813a3cb..7bed3ac 100644 --- a/ipa/general/0104.md +++ b/ipa/general/0104.md @@ -14,7 +14,7 @@ example, `/groups/{groupId}/clusters/{clusterName}`) to retrieve that resource. - The purpose of the Get method is to return data from a single resource - The HTTP verb **must** be [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) -- The method **must not** [cause side effects](0103.md) +- The method **must not** [cause side effects](0103.mdx) - The request **must not** include a body - API producers **should** implement as a `Response` suffixed object - A `Response` object **must not** include fields available only on creation diff --git a/ipa/general/0105.md b/ipa/general/0105.md index 3ed5b52..92f67d4 100644 --- a/ipa/general/0105.md +++ b/ipa/general/0105.md @@ -16,7 +16,7 @@ which lives within that collection. - The purpose of the List method is to return data from a finite collection - The HTTP verb **must** be [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) -- The method **must not** [cause side effects](0103.md) +- The method **must not** [cause side effects](0103.mdx) - The request **must not** include a body - The response body **should** consist of the same resource object returned by the [Get](0104.md) method diff --git a/ipa/general/0114.md b/ipa/general/0114.md index 350048d..d84f983 100644 --- a/ipa/general/0114.md +++ b/ipa/general/0114.md @@ -22,7 +22,7 @@ error-handling code. in case of an error by adding a `help` field - `help` **must** include a short description as description - `help` **must** include a link to the documentation url -- [Methods](0103.md) **must** document any possible error and their associated +- [Methods](0103.mdx) **must** document any possible error and their associated [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) ### Authentication and Authorization