NextGen APIs

REST APIs Design Guidelines

Table of contents

General guidelines

MUST write APIs in English (U.S.)
MUST secure endpoints

Security

MUST use standard data formats

OpenAPI Specification Data Types

URLs

Common rules

MUST use all lowercase characters

For every part that composes the URI of an API, all characters must be lowercase.

DO

  • /path

DON’T

  • /Path

  • /myPath

  • /PATH

MUST use kebab-case

For every part that composes the URI of an API, every multiple-worded section must use kebab-case.

DO

  • /kebab-case-part

DON’T

  • /camelCasePart

  • /PascalCasePart

  • /onewordpart

  • /snake_case_part

Path structure

Product API: {hostname}/{context-path}/api/domain/subdomain/version/resource

Project API: {hostname}/{context-path}/api/domain/subdomain/project/version/resource

Examples

  • {hostname}: s000009.dev.tech.eu1.platform.overit.cloud

  • {context-path}: fsm

MUST use normalised paths

Do not specify paths with duplicate or trailing slashes. Also, do not use path variables with empty string values.

DO

  • /resources-a/addresses

  • /resources-b

DON’T

  • /resources-a//addresses

  • /resources-b/

API

The /api prefix is used to underline that the following part of the URI identifies a REST API resource, not something else. Since the NextGen Platform does not publish REST APIs only, there is a specification prefix, used to define other published content such as SOAP services and user interfaces.

Domain

The name of the functional domain (DDD) that groups all the published resources under that domain.

  • Warehouse domain: api/warehouse/subdomain

  • Custom Warehouse domain: api/warehouse/subdomain/project/

Subdomain

The name of the functional subdomain (DDD) that groups a subset of the resources published in its domain. All APIs published in the same subdomain will be versioned together, as you will see in the Version chapter.

  • Warehouse domain, Material subdomain: api/warehouse/material/

  • Custom Material subdomain: api/warehouse/material/project/

Project

The acronym of the project is already being used to signal database custom objects. It must start with the x letter and then the actual project acronym will follow prj. This is mandatory for new custom APIs as well as customizations of standard APIs.

Custom API: api/domain/subdomain/xprj/version/resource

Version

The version must be indicated with the r letter and the version number, expressed with a single number and no special characters. Beware that every API in a subdomain will be versioned together, so any breaking change suffered by even a single API, will cause the entire subdomain to be versioned.

Versioned API: api/domain/subdomain/r3/resource

Resource

The name of the actual resource that a consumer will access through the API. It can be a real resource or a virtual resource (intent, explained after).

When publishing relationship-type APIs, MUST use the parent/child path format.

Use intent-resources when publishing relationships with metadata.

Use resources without the parent/child format when publishing resources that exist beyond their relationships (e.g. materials belonging to a work order operation are not a parent/child relationship, they must be named properly as a resource).

SHOULD avoid intent resources whenever possible

https://overit-spa.atlassian.net/wiki/spaces/NEXTGENFSMAPIS/pages/edit-v2/102921896206#Intent-resource

https://overit-docs.atlassian.net/wiki/pages/createpage.action?spaceKey=PAR&title=REST%20API%3A%20Verbs

MUST use nouns, MUST NOT use verbs

DO

  • PUT /resources

  • POST /resources

DON’T

  • /changeresources

  • /createresources

MUST use plural resource name

DO

  • PUT /resources

  • GET /resources

DON’T

  • PUT /resource

  • GET /resource

MUST use domain-specific resource names

Using domain-specific nomenclature for resource names helps developers to understand the functionality and basic semantics of your resources. It also reduces the need for further documentation outside the API definition. This guideline can be lifted when defining a resource inside its domain, in that case, the specificity related to the domain MAY be omitted.

DO

  • /work-orders/r1/work-cycles

  • /work-orders/r1/work-orders/{workOrderId}/operations

  • /work-orders/r1/work-cycles/{workCycleId}/operations

DON’T

  • /work-orders/r1/cycles

  • /r1/operations

  • /work-orders/r1/cycles/operations

MUST avoid using duplicate names when the subdomain coincides with the domain

Use the subdomain name only when necessary, meaning that the subdomain does not coincide with the domain itself.

DO

  • /work-orders/r1/work-orders

  • /work-orders/r1/work-orders/{workOrderId}/operations

  • /work-orders/r1/work-cycles/{workCycleId}/operations

  • /work-orders/projects/r1/projects

DON’T

  • /work-orders/work-orders/r1/work-orders

  • /work-orders/r1/work-orders/{workOrderId}/work-order-operations

  • /work-orders/r1/work-cycles/{workCycleId}/work-cycle-operations

  • /work-orders/r1/projects

  • /projects/r1/projects

SHOULD limit the number of nested resources

Keep the nesting limited to main resources and sub-resources. Use sub-resources if their life cycle is (loosely) coupled to the main resource, meaning that the main resource acts as a collector of sub-resources. Use < 3 nesting levels. More levels increase API complexity and URL path length (max URL length = 2000 characters).

Use sub-resources also for relationships between resources, instead of creating a relationship resource.

DO

  • POST /collection-one/{itemOneId}/collection-two

    • Creates a new resource of collection-two which is a sub-resource of itemOne of collection-one.

    • e.g. POST /work-orders/{workOrderId}/work-order-operations creates a new Work Order Operation which is a sub-resource of a Work Order.

  • GET /collection-one/{itemOneId}/collection-two/{itemTwoId}

    • Retrieves all data related to itemTwo of collection-two which is a sub-resource of itemOne of collection-one.

    • e.g. GET /work-orders/{workOrderId}/work-order-operations/{workOrderOperationId} retrieves the data of a Work Order Operation which is a nested sub-resource of a Work Order.

  • PUT /collection-one/{itemOneId}/collection-two

    • Replaces the existing relationships between itemOne of collection-one and collection-two resources.

    • e.g. PUT /operation-centers/{operationCenterId}/addresses replace all existing relationships between an Operation Center and its Addresses with new ones.

  • GET /collection-one/{itemOneId}/collection-two

    • Retrieves the IDs of all the items of collection-two that are related to itemOne of collection-one

    • e.g. GET /operation-centers/{operationCenterId}/addresses retrieve all the existing relationships (IDs) between an Operation Center and its Addresses.

DON’T

  • POST /collection-one/{itemId}/collection-two/{itemId}/collection-three

  • GET /collection-one/{itemId}/collection-two/{itemId}/collection-three/{itemId}

  • PUT /collection-one/{itemId}/relationship-between-one-two

  • GET /collection-one/{itemId}/relationship-between-one-two

MAY use the self pseudo-identifier to express the logged-in user information

When referring to resources related to the currently logged-in user the self pseudo-identifier.

DO

  • GET /resource/self

  • GET /resource/{resourceId}/self

DON’T

  • /users/resource/self

  • /users/self/resource

  • /resource/me

  • /resource/myself

  • /current/resource

Intent resource

Every action that does not fall into a RESTful bucket must be designed as a sub-resource following RESTful principles (described for resources just above). The name of the resource should be as clear as possible and very well-documented, since it is not a RESTful resource a consumer must be well-informed about the effects of invoking such an endpoint. Think about it as a form that becomes an information resource describing what a client wants the server to do. An example would be:

GitHub APIs: star and unstar a gist
  • PUT /gists/{gist_id}/star

  • DELETE /gists/{gist_id}/star

As far as REST is concerned, the spelling of the URI absolutely does not matter; but from the point of view of a human readable naming convention, start from the fact that the resource is the document, not the side effect that you want the document to have. So, for example, it's totally normal and compliant with REST that the document that describes the current state of an entity and the document that describes changes you want to make to the entity are different documents with different identifiers.

Having said that, when designing an intent there are a few key points to pay attention to:

  • MUST use plural nouns instead of verbs, e.g. /activations instead of /activate

  • Intents performed on a singular resource MUST be designed as a sub-resource of a resource instance, e.g. /work-orders/{workOrderId}/activations

  • Possible pseudo-identifiers (such as /self or /batch) MUST come before the intent name, e.g. /work-orders/batch/activations

DO

  • /resource/intents

  • /resource/{resourceId}/intents

  • /resource/pseudo/intents

DON’T

  • /intents/{resourceId}

  • /intents/resource

  • /resource/intent

Duplicate resource

Since we currently have several products that contribute to the NextGen APIs catalog, there might be REST resources that clash between different products because both products need to manage the same resource.

The correct solution is to decide which product is the owner of that REST resource and let it publish every needed API for the resource.

If the correct solution is not applicable, a new API is needed, and it must not be the same; there cannot be a clash between different resources from different products. Therefore, the URL (or better, the complete name) of the REST resource must change. An owner of the resource must be identified, and all its APIs do not need to change names. For every other product that is not the owner of the resource, an additional part of the URL MUST be changed: it represents the acronym or short name for that product and it coincides with the context-path variable.

  • NextGen Field Collaboration: /fc

  • NextGen GEO: /geo

  • NextGen FSM: /fsm

This way, the entire domain and subdomain can be versioned according to the specific product, and the owner remains independent from others' evolutions.

DO

  • /{context-path}/api/domain/subdomain/version/resource

  • e.g.: /fc/api/work-orders/execution/r1/outcome-classes

Asynchronous resources

In case of asynchronous resources, meaning APIs that will publish a message in an Event Streaming Queue (e.g.: SNS, Kafka, …) the pseudo to be used is /async and it will be placed after the resource and before any other pseudo or intent.

DO

  • /resource/async/intents

  • /resource/{resourceId}/async

DON’T

  • /async/{resourceId}

  • /async/resource

Data and parameters

Common rules

MAY use acronyms when they are well-known

DO

  • workOrderSLA where SLA is the well-known acronym for Service Level Agreement

  • workOrderServiceLevelAgreement

DON’T

  • workOrderServicelevelagreement

  • workOrderPCMCIA where PCMCIA stands for People Can’t Memorize Computer Industry Acronyms which is a not-so-well-known backronym

MUST use uppercase letters for acronyms

DO

  • appointmentSLA where SLA is the well-known acronym for Service Level Agreement

DON’T

  • appointmentSla

  • appointmentsla

Path Params

MUST use entityId to name parameters

DO

  • {resourceId}

DON’T

  • /{Id}

  • /{id}

  • /{param}

MUST use camelCase

DO

  • {resourceId}

DON’T

  • {resource-id}

  • {ResourceId}

  • {resourceid}

  • {resource_id}

  • {RESOURCEID}

Query Params

MUST use camelCase

DO

  • /resources?camelCaseQueryParam=value

DON’T

  • /resources?kebab-case-query-param

  • /resources?PascalCaseQueryParam

  • /resources?onewordqueryparam

  • /resources?snake_case_query_param

MUST explode filters for Collections

GET operations which allow for filtering results, the filter input data MUST be declared exploded (meaning one QueryParam per filter field). This is done to allow collections of data as a single filter parameter.

DO

  • /resources?myParamField1=value&myParamField2=value1,value2,value3&myparamField3=value

DON’T

  • /resources?filter=myParamField1,myValue1,myParamField2,myValue2

MAY use complex Query Parameters for Objects

In case of Objects that needs to be passed as Query Parameters (such as the Page object, see also Pagination and How to use APIs: Pagination) you MAY use a non-exploded Query Parameter.

DO

  • /resources?page=size,10,num,1&myObject=pippo,1,pluto,2

SHOULD use resource specific filter names

Using resource-specific nomenclature for attributes names helps developers to understand the functionality and basic semantics of your filter attributes. It also reduces the need for further documentation outside the API definition.

This guideline becomes a MUST whenever the resources are multiple, such as in an orchestration.

This guideline can be lifted when defining a filter for a single resource inside its own domain, in that case the specificity related to the domain MAY be omitted.

Payload

MUST use camelCase

DO

  • {"camelCaseField": ""}

DON’T

  • {"kebab-case-field": ""}

  • {"PascalCaseField": ""}

  • {"onewordfield": ""}

  • {"snake_case_field": ""}

MUST use Id suffix in foreign key fields

DO

  • {"myForeignKeyId": 33}

DON’T

  • {"myForeignKey": ""}

  • {"Id": ""}

  • {"myId_myForeignKey": ""}

Response MUST be compliant with Problem Details specification https://datatracker.ietf.org/doc/rfc9457/

The format of the response of every API MUST be exactly the one defined by the RFC 9457. An example follows but for more information please consult the RFC guideline:

JSON
POST /details HTTP/1.1
Host: account.example.com
Accept: application/json

{
  "age": 42.3,
  "profile": {
    "color": "yellow"
  }
}

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Content-Language: en

{
  "type": "https://example.net/validation-error",
  "title": "Your request is not valid.",
  "errors": [
    {
      "detail": "must be a positive integer",
      "pointer": "#/age"
    },
    {
      "detail": "must be 'green', 'red' or 'blue'",
      "pointer": "#/profile/color"
    }
  ]
}

HTTP status codes

MAY use any standard HTTP status code

As per https://developer.mozilla.org/en-US/docs/Web/HTTP/Status an API designer may use any of the standard HTTP status codes. WebDAV status codes are excluded and prohibited.

MUST document client-side errors

In case of 400-499 errors, meaning client-side errors, there must be a complete and thorough explanation of the error and how can the client fix the request to solve it.

SHOULD stick to a maximum of three error codes

To not overcomplicate the client-side error management, try to not generate more than 3 different error codes. If you need more than 3 error codes to be returned, try to group similar errors into the same code and differentiate the error description better.

In any case, it is not prohibited to handle more than 3 codes, especially when for certain APIs there must be several different client-side error management procedures, just be sure to properly document every error code and why it’s needed.

MUST use concurrency issues error codes

If an API handles concurrency, be sure to use the proper HTTP status code to inform the client of the concurrency-type error.

MAY document banal status codes in a common documentation space

For both private and public APIs, there can be a common documentation space dedicated to banal status codes. These are, for instance, 404 Not Found, 200 OK, and so on.

This MUST be done if not documenting every possible status code in APIs, more information below, separated between the private API Suite and the public API Catalog.

Common response HTTP codes

Codes that might always be returned:

403 (FORBIDDEN)

500 (INTERNAL SERVER ERROR)

GET

  • /users/{userId}

    • 200 (OK)

    • 404 (NOT FOUND)

  • /users?name=pippo

    • 200 (OK) even if a user named pippo does not exist, the response MUST be 200 (OK) with an empty list

  • /users?nonExistentParameter=-1

    • 400 (BAD REQUEST)

POST

  • /users

    • 201 (CREATED)

  • /users?nonExistentParameter=-1

    • 400 (BAD REQUEST)

  • /users with a non-compliant request payload

    • 400 (BAD REQUEST)

  • /resources/{resourceId}/children

    • 201 (CREATED)

    • 404 (NOT FOUND) if resources/{resourceId} does not exist

PATCH

  • /users/{userId}

    • 204 (NO CONTENT)

    • 404 (NOT FOUND)

PUT

  • /users

    • 201 (CREATED)

    • 204 (NO CONTENT) in case of an update

  • /users/{userId}

    • 204 (NO CONTENT)

    • 404 (NOT FOUND)

  • /users?nonExistentParameter=-1

    • 400 (BAD REQUEST)

  • /users with a non-compliant request payload

    • 400 (BAD REQUEST)

  • /resources/{resourceId}/children

    • 201 (CREATED)

    • 204 (NO CONTENT) in case of an update

    • 404 (NOT FOUND) if resources/{resourceId} does not exist

DELETE

  • /users/{userId}

    • 204 (NO CONTENT)

    • 404 (NOT FOUND)

  • /users?nonExistentParameter=-1

    • 400 (BAD REQUEST)

Private API Suite

SHOULD document only non-banal codes

The complete documentation of all status codes can be avoided for private APIs, since they are internally managed.

Public API Catalog

SHOULD document every status code

In the case of public APIs, every status code that can be returned to the consumer SHOULD be documented. Completely banal status codes MAY be avoided when deemed absolutely not necessary.

Configurability

MUST prefix parameters with the _ (underscore) character

Every configuration-related parameter MUST be prefixed with the _ (underscore) character to increase readability and to allow anyone to use one of the reserved keywords as another parameter.

Pagination

SHOULD implement pagination in GET operations

The implementation and publishing of a paginated GET operation is strongly advised. Having said that, it is not mandatory since not every GET operation will access a resource which needs pagination. Typically the pagination mechanism is combined with the ordering mechanism, explained below. Pagination

JSON
"_page" : {
  "num": 1,
  "size": 10
}
Bash
GET /resource?_page=num,1,size,10

OpenAPI Specification

QueryParameter
YAML
- name: _page
  in: query
  required: true
  style: form
  explode: false
  schema:
    $ref: '../my-schemas.yaml#/components/schemas/Page'
QueryParameter schema
YAML
Page:
  required:
    - num
  type: object
  properties:
    num:
      type: integer
      description: "This parameter is an Integer that allows the client to specify which page number it wants 
              to retrieve. The default value is zero, which means that the first page will be retrieved in case 
              the Page object is not specified by the client"
      format: int32
    size:
      type: integer
      description: "This parameter is an Integer that allows the client to specify the maximum number of entries 
              that a single page is allowed to contain.If the client does not respect the Page contract, meaning that it 
              specifies the Page object but not its num attribute, an error 400 Bad Request is generated and returned to 
              the client."
      format: int32
Response object schema
YAML
PageResponse:
  type: object
  properties:
    page:
      type: object
      properties:
        num:
          type: integer
          description: "The number of the page retrieved."
        has-more:
          type: boolean
          description: 'true if more results are present, false otherwise.'

Order

SHOULD implement ordering in GET operations

The implementation and publishing of a sortable GET operation is strongly advised. Having said that, it is not mandatory since not every GET operation will access a resource which needs ordering. Typically the ordering mechanism is combined with the pagination mechanism, explained above.

To specify the ordering you MUST use a Query Param, the keyword MUST be order and the type of the parameter MUST be a string:

JSON
"_order" : "+field1,-field2"
Bash
GET /resource?order=+field1,-field2

To request an ascending order, add as prefix the character + (plus).

To request a descending order, add as prefix the character - (minus or hyphen).

MUST document every possible order of a resource

When a consumer is allowed to order the results of a GET operation, there must be a documentation of every possible order published by the API.

If a resource is particularly complex or numerous, there MUST be a documentation of an ordered subset of fields by which a consumer is allowed to order. To specify the order the documentation MUST provide an alias that the consumer can use as attribute name.

If a resource is particularly simple and not numerous, there MAY be a free ordering policy on the API.

OpenAPI Specification

YAML
"name" : "_order",
"in" : "query",
"schema" : {
  "type" : "string"
}

Language

MUST allow a consumer to specify the desired language

A consumer MUST be able to specify the desired language as a Query Parameter of a GET request. The possible values are the following:

  • ALL: Ask for all available translations

  • USER: Ask for the current user translation

  • IANA 2-char code: Ask for a specific translation using the IANA 2-char code that identified a language

JSON
"_language" : "ALL,USER,EN"

OpenAPI Specification

QueryParameter
YAML
- name: language
  in: query
  required: false
  schema:
    type: string
    default: user
  examples:
    LangUser:
      value: user
      summary: User's language
    LangAll:
      value: all
      summary: All languages
    LangIANA:
      value: en
      summary: IANA compliant language
Response object schema
YAML
LocalizedItem:
  required:
    - "lang"
  type: "object"
  properties:
    lang:
      type: "string"
    value:
      type: "string"

Response fields

MUST implement configurability of the data returned in GET operations

Every GET operation must allow consumers to specify a subset of data that they need to be returned by the API.

For this purpose, you MUST use a Query Param, the keyword MUST be _fields and the type of the parameter MUST be an array of string.

JSON
"_fields" : [ "admin", "editor", "contributor" ]

OpenAPI Specification

QueryParameter
YAML
"name" : "_fields",
"in" : "query",
"schema" : {
  "type" : "array",
  "items" : {
    "type" : "string"
  }
}

Extension

MUST implement extensibility of all data in an API

Every HTTP verb must implement the extensibility mechanism, in the form of a Query Parameter for filtering, and a JSON object for request and response data.

OpenAPI Specification

The format when extending QueryParameters is a prefix named precisely extension. (dot), which must be followed by the name of the desired extended field name, i.e. extension.customCode=value. The extended attributes can be repeated to obtain collections of extended attributes, i.e.: extension.customCodes=A&extension.customCodes=B.

QueryParameter
YAML
- in: query
  name: extension
  schema:
    type: object
    patternProperties:
      # Parameter names
      '^(extension\.)[A-Za-z][A-Za-z0-9]*$':
        # Parameter values
        type: array
    additionalProperties: false
    examples:
      - extension.myFieldName: myValue
Request or response object schema
YAML
extension:
  type: object
  description: Extended fields and their respective values
  additionalProperties: true
  examples:
    - customCode: myCustomCode

Java implementation

QueryParameter
Java
@RestController
@RequestMapping("/api/test/")
public class TestRestController {

    @GetMapping("test1")
    public UserFilter test1(@ParameterObject UserFilter filter, @RequestParam MultiValueMap<String, Object> extension) {
        Map<String, Object> filtered = new HashMap<>();
        extension.entrySet()
            .stream()
            .filter(entry -> entry.getKey().startsWith("extension."))
                .forEach(entry -> {
                    if (entry.getValue().size() == 1) {
                        filtered.put(normalizeExtensionKeyName(entry), entry.getValue().get(0));
                    } else {
                        filtered.put(normalizeExtensionKeyName(entry), entry.getValue());
                    }
                });

        filter.setExtension(filtered);
        return filter;
    }
    
    private String normalizeExtensionKeyName(Map.Entry<String, ?> entry) {
        return entry.getKey().replace("extension.", "");
    }
}
Response object

WORK IN PROGRESS

Expanding Work in progress

Expanding related resources (also known as Resource embedding) is a great way to reduce the number of requests. In cases where clients know upfront that they need some related resources, they can instruct the server to prefetch that data eagerly. Whether this is optimized on the server, e.g., a database join, or done in a generic way, e.g., an HTTP proxy that transparently expands resources, is up to the implementation.

WARNING: Expanding is not a substitute for orchestration.

Any resource that does not belong in the same domain MUST be accessed separately using an orchestration layer.

Expanding a sub-resource can look like this, where an order resource has its order operations as a sub-resource /work-orders/{workOrderId}/operations:

JSON
GET /work-orders/123?expand=operations HTTP/1.1

{
  "id": "123",
  "description": "Order number 123",
  "...": "..."
  "operations": [
    {
      "order": 1,
      "description": "1234-ABCD-7890",
      "...": "..."
    }
  ]
}

Batch operations

Batch operations MUST be published via the proper verb and a collection resource. To easily identify such operations the keyword batch MUST be used in the resource URI, after the resource specific name, e.g. POST /work-orders/batch is an endpoint which allows POSTing several work orders in one invocation.

Documentation

MUST document server-side validation policies

When an API implements at least one validation mechanism beyond its OAS specification, the validation process and the mandatory data must be documented. This also includes fields which are defined as optional in the OAS specification, but are mandatory in certain circumstances.

MUST tag every resource with its subdomain name

When documenting a REST resource, one MUST use the OpenAPI tag feature and the tag itself MUST be equal to the subdomain name to which the resource belongs. Example:

Resource: /domain/subdomain/r1/my-resource

Tag: Domain / Subdomain

YAML
paths:
  /domain/subdomain/my-resource:
    get:
      tags:
      - Domain / Subdomain
      summary: Get a resource
      description: "Returns the complete resource instance with all relevant information.
Java
@Configuration
    public static class Identity {
        public static final String TAG_NAME = "Configurations / Identity";
        public static final String TAG_DESCRIPTION = "Services to manage the information and the operations about the identity";
        public static final String GROUP_NAME = "configurations-identity";

        @Bean
        GroupedOpenApi identityApi() {
            return GroupedOpenApi.builder()
                    .group(GROUP_NAME)
                    .displayName(TAG_NAME)
                    .pathsToMatch("/api/configurations/identity/**")
                    .build();
        }
    }

Validation

MUST validate the intra-subdomain consistency of the CRUD operations

The consistency of a single API CRUD operation must be validated. This has to be done only against the resource itself or against resources in the same subdomain as the resource. Validations to be done are both technical and functional, some examples include: start date must be before the end date; denormalized keys towards resources (in the same subdomain) must exist, be consistent, and the linked resource must be functionally able to receive the link itself.

Every validation against resources outside the subdomain of the resource MUST be done in an outer layer, i.e. an orchestration layer.