API Guidelines

Overview

Creating great APIs that are consistent and straight-forward encourages other developers to use them. In turn, they may add value to your API, and then share it with others. This regenerative cycle is the foundation on which YaaS is built. To succeed at developing great APIs, there are a lot of things to consider before you begin. Read these best practices for guidance on things like RAML modeling, naming, and error responses.


API Definition with RAML

YaaS services use RAML, the RESTful API Modeling Language, to describe their own API in a natural and intuitive way.

Exposing the RAML API Definition

In order to make a service easily accessible to (potential) API consumers, the service itself must expose its API definition as a RAML file. The service must expose the RAML API definition through HTTP at a well-defined URL. The location can be resolved relative to the base-URL of the service itself, using the following relative URL:

meta-data/api.raml

Consider the following example:

Service Base-URL: https://api.beta.yaas.io/hybris/configuration/v1/ RAML API Definition URL: https://api.beta.yaas.io/hybris/configuration/v1/meta-data/api.raml

Thus the RAML file at meta-data/api.raml serves as the entry point to the API definition of a service.

Naturally, this RAML file may reference additional resources, like RAML traits and resource-types, JSON Schemas, or example data. While (potential) consumers of the API will process such additional resources, they should not make any unwarranted assumptions about them. In particular, they must not assume that names or contents of particular additional resources remain stable over time.

Structuring RAML Resources

That said, authors of RAML API definitions are encouraged to follow certain conventions for structuring and naming their additional resources. According to these conventions, the following relative URL paths and file suffixes should be used:

ResourcePathSuffixExample
RAML API Definitionmeta-dataapi.ramlmeta-data/api.raml
RAML Traitmeta-data/traits/.ramlmeta-data/traits/colored.raml
RAML Resource Typemeta-data/resourceTypes/.ramlmeta-data/resourceTypes/items.raml
RAML Security Schememeta-data/securitySchemes.ramlmeta-data/securitySchemes/oauth_2_0.raml
JSON Schemameta-data/schemas/.jsonmeta-data/schemas/thing.json
Example Datameta-data/examples/-example.jsonmeta-data/examples/thing-example.json

References between these resources of the RAML API definition should be made by relative URLs.

Expansion of RAML Resources

Optionally, a service may provide expanded views of its meta-data/api.raml file, which can facilitate processing of the RAML API definition by consumers. If supported, these views can be activated by a query parameter named expand that supports the following values:

ValueView Semantics
noneNo expansion is performed. That is, the original meta-data/api.raml is passed through unchanged. This is the default.
compactExternal RAML resources are inlined transitively. Definitions from traits and resource-types are applied to the respective resources and actions. JSON Schemas are also inlined, but in a shallow manner. That is, references inside the JSON Schemas are not inlined.
fullExternal RAML resources are inlined transitively. Definitions from traits and resource-types are applied to the respective resources and actions. JSON Schemas are also inlined transitively. That is, references inside the JSON Schemas are inlined, too.

If the service does not support a particular view, it may default to not performing any expansion whatsoever. That behavior corresponds to expand=none.


Naming Services

Service names are in lowercase, alphanumeric characters, with a maximum of 16 characters. The service name becomes part of the URL, such as api.yaas.io/products. Only the dash (-) special character is accepted, and it is used to separate implementations of the same interface or APIs of the same type, such as tax-avalara. The standard, out-of-the-box implementation is the simple name, such as tax.


Naming Resources

Use nouns, not verbs, when naming resources, and use plural nouns such as /products or /carts/1/items. If this is not possible, then use action links, as discussed in the Operations section.


Naming Attributes

Use lower camel case, such as createdAt.


Naming Query Parameters

Use lower camel case, such as totalCount.


Naming Events

For information about Events including the naming convention, see the Events documentation of the Development Guidelines.


Naming Scopes

For information about scopes including the naming convention, see the Scopes section of the Security page.


Naming Headers

Use a prefix indicating a custom namespace for custom headers, such as hybris-tenant, instead of an X-prefix.


Errors

Every service in YaaS uses the common error message schema for error response payloads.

Error type definition

An error response always contains these attributes:

  • status – The original response code of the response.
  • type – A Hybris-specific error type for classification of the error, as described in the table below.
  • message – A message describing the problem in relation to the error type in human-readable form.
  • moreinfo – A link to get more information on that kind of error type.

Example error message

{
    "status": 500,
    "type": "internal_server_error",
    "message": "Some unexpected internal error occurred, please contact support yaas.support@yaas.hybris.com",
    "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

An error can be due to multiple issues, or it can be specified in more detail using a more fine-grained error type. The error response contains an optional list of details, and the error detail consists of these attributes:

  • field – A bean notation expression specifying the element in the request data causing the error, such as product.variants[3].name. This can be empty if the violation is not field-specific.
  • type – A Hybris-specific error type for classification of the detailed error. The type must be a sub-type of the type specified in the common error payload.
  • message – A message that describes the problem that is more specific to the sub-type.
  • moreInfo – A link to information related to the sub-error type.

Example error response with details

{
    "status": "400",
    "type": "validation_violation",
    "message": "Several constraints violations while validating 'product'",
    "moreInfo" : "https://pattern.yaas.io/errortypes.html",
    "details":
    {
      [
        {
          "field": "Product.code",
          "type": "missing_field",
          "message": "A field expected but found null",
          "moreInfo" :"https://devportal.yaas.io/services/configuration/latest/index.html#Project-SpecificConfiguration"
        },
        {
          "field": "Product.sku",
          "type": "invalid_field_pattern",
          "message": "A field expected to match a pattern (nn-nn-nn) but found  (123456)"
        }
      ]
    }
}

Service SDK Support

The Service SDK provides support for easily setting up Jersey applications using the error response format described above. For that, the Service SDK's Jersey Support library provides Jersey ExceptionMapper for the most common exceptions. Furthermore, the Service SDK provides constants and error schema DTOs as part of the Pattern Support library.

Glossary of error types

The table below includes a list of possible error types, their response codes, descriptions, and suitable messages. The Error Description column describes different error situations in more detail for developers who use the message attribute of an error response. The Recommended Error Message column contains suggestions for user-related error messages to display in front-ends. In general, response codes are divided into 4xx for client-side errors, and 5xx for server-side errors:

HTTP CodeTop-Level Error TypeSub-Level Error TypeError DescriptionRecommended Error Message
400bad_payload_syntaxThe request payload has incorrect syntax according to the sent Content-Type. Check the payload content for syntax errors, such as missing commas or quotation marks that are not closed.Something went very wrong. Please try again.
400validation_violationmissing_fieldThe requested body payload for a POST or PUT operation is missing, which violates the defined validation constraints. This denotes a missing field when a value is expected.We need all required fields complete to keep you moving.
400invalid_fieldThe requested body payload for the POST or PUT operation violates the validation constraints. This denotes specifically that there is:
  • A type format incompatibility, such as specifying a pattern, but the given value does not match the pattern.
  • A type incompatibility, such as a field modeled to be an integer, but a non-numeric expression was found instead.
  • A range under or over flow validation violation cause.
We need all your entries to be correct to keep you moving.
400missing_headerAny of the required headers is missing. Check the request to see if it contains the required header, or refer to the error details response to identify the missing header.Something went very wrong. Please try again.
400invalid_headerOne or more sent headers has an invalid format. This denotes specifically that there is:
  • A type format incompatibility, such as specifying a pattern, but the given value does not match the pattern.
  • A type incompatibility, such as a field modeled to be an integer, but a non-numeric expression was found instead.
  • A range under or over flow validation violation cause.
  • A value is longer than the maximum length.
Something went very wrong. Please try again.
400missing_query_parameterA required query parameter is missing. Check your request to ensure that it contains the required query parameters.Something went very wrong. Please try again.
400invalid_query_parameterOne or more sent query parameters has an invalid format. This denotes specifically that there is:
  • A type format incompatibility, such as specifying a pattern, but the given value does not match the pattern.
  • A type incompatibility, such as a field modeled to be an integer, but a non-numeric expression was found instead.
  • A value is longer than the maximum length.
Something went very wrong. Please try again.
400invalid_uri_parameterOne or more URI parameters have an invalid format in a request with creation semantic (usually POST on a specific resource). This denotes specifically that there is:
  • A type format incompatibility, such as specifying a pattern, but the given value does not match the pattern.
  • A type incompatibility, such as a field modeled to be an integer, but a non-numeric expression was found instead.
  • A value is longer than the maximum length.
For any non-creation semantic, like for a GET, a 404 response should be returned instead.
Something went very wrong. Please try again.
400out_of_range_parameterA query parameter has an invalid format. This could be due to a range under or over flow validation violation cause. Check your request to ensure it contains valid query parameters.Something went very wrong. Please try again.
400business_errorimaginary_product_unavailableA business error indicating that a product with an imaginary description is not in the inventory.Whoops! We can't find what you're looking for. Please try again.
400business_erroryour_api's_specific_typeEnter your description.We need all required fields complete to keep you moving.
401insufficient_credentialsNo credentials are provided, or the provided credentials are refused.Something went very wrong. Please try again.
403insufficient_permissionsThe credentials indicate a system-known user, but the user is not allowed to perform the requested operation.Something went very wrong. Please try again.
404element_resource_non_existingThe requested URI does not map to a resource.Whoops! We can't find what you're looking for. Please try again.
405unsupported_methodThe requested resource does not support the specified HTTP action. Verify that you are calling an action supported by the requested endpoint.Something went very wrong. Please try again.
406unsupported_response_content_typeThe client requested a response type which cannot be obtained from the requested resource. Check the requested endpoint response capabilities and compare them with your client Accept header.Something went very wrong. Please try again.
409conflict_resourceThe requested resource could not be created, updated, or deleted due to server-side validation. This could be caused either by a temporary issue, such as an optimistic lock or persistent data constraint. Refer to the error details response to identify conflict details.Something went wrong. Please try again.
409unique_index_violationThe requested resource could not be created due to a uniqueness constraint. Assure that no resource exists having the same identifying attributes.Something went wrong. Please try again.
413bad_payload_sizeThe request payload is larger than the server can process.Something went wrong. Please try again.
414uri_too_longThe Request-URI is longer than allowed. This can occur when a client has improperly converted a POST request to a GET request with long query information or when the client has descended into a URI endless redirection, such as a redirected URI prefix that points to a suffix of itself. Also, it can occur when the server is under attack by a client attempting to exploit security holes present in some servers using fixed-length buffers for reading or manipulating the Request-URI.Something went wrong. Please try again.
415unsupported_request_content_typeThe client sent a type of request which is not supported by the requested endpoint. Verify that your request's Content-Type matches the requested endpoint accepted content.Something went wrong. Please try again.
500internal_service_errorA server-side exception occurred that prevented the system from correctly returning the result. Refer to the error details response to identify exception details.Something went very wrong. Please try again.
500backing_service_unavailableSome backing service is not available. Refer to the error details response to identify the origin of the problem.Something went very wrong. Please try again.
503service_temporarily_unavailableA temporary service unavailability was detected. Refer to the error details response for a re-attempt strategy.Something went very wrong. Please try again.

Constraint violation error handling

In general, there are two categories of constraints defined in the API definition. One category is specified in the JSON schema definition, which affects request and response payloads. The second category is specified by the RAML definition itself, which affects the related resource URL.

The following constraints of a JSON schema definition should all result in a 400 Bad Request response with sub type invalid_field:

JSON TypeConstraints
stringmaxLength, minLength, pattern, format: email
integerminimum, exclusiveMinimum, maximum, exclusiveMaximum
arrayminimum, minItems, maxItems

For more information about what these constraints mean, refer to the JSON Schema Validation Specification

The RAML specification enables you to provide a set of attributes, or named parameters, for properties modeled in the RAML content. An overview of constraint violations on which RAML element should return which kind of error is listed in this table:

RAML ElementHTTP CodeTop-Level Error TypeSub-Level Error Type
uriParameters404element_resource_non_existing-
headers400validation_violationinvalid_header
queryParameters400validation_violationinvalid_query_parameter

Non-error scenarios

ScenarioHTTP Code
successful GET200 OK
successful POST201 Created (+ Location header), 202 Accepted
successful PUT204 No Content
successful PATCH204 No Content
successful DELETE204 No Content
Authentication of OAuth get access token302 Found

Authentication and authorization error responses

In order to access a service through a proxy in your request, you need to use an Authorization header with the Bearer access_token_value attribute. If the access token is invalid, you receive an error message with the proper information in the body.

No authorization header

This response happens when a no authorization header is given or the format is not correct. For example:

HTTP/1.1 401 Unauthorized
{
  "status" : 401,
  "message" : "Authorization: Unauthorized. Bearer TOKEN is missing",
  "type" : "insufficient_credentials",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Invalid access token

This response happens when the given access token is invalid. For example:

HTTP/1.1 401 Unauthorized
{
  "status" : 401,
  "message" : "Authorization: Unauthorized. Bearer TOKEN is invalid",
  "type" : "insufficient_credentials",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Lack of subscription

This response happens when the application does not have permission to access the proxy due to a lack of subscription to the product. For example:

HTTP/1.1 401 Unauthorized
{
  "status" : 401,
  "message" : "Application does not have permission to access the proxy due to lack of subscription to the product.",
  "type" : "insufficient_credentials",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Invalid scope

This response happens when the access token is valid, but you do not have permissions to that scope. For example:

HTTP/1.1 403 Forbidden
{
  "status" : 403,
  "message" : "Access denied - invalid scope",
  "type" : "insufficient_permissions",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Cannot find the service

This response happens when the service path does not exist. For example:

HTTP/1.1 404 NotFound
{
  "status" : 404,
  "message" : "Service does not exist",
  "type" : "element_resource_non_existing",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Unknown server error

This response happens when there are other internal server errors. For example:

HTTP/1.1 500 InternalServerError
{
  "status" : 500,
  "message" : "Invalid server settings. Please contact administrator.",
  "type" : "internal_service_error",
  "moreInfo" : "https://pattern.yaas.io/errortypes.html"
}

Error response propagation

In the example below, an external client calls Service A and subsequently calls Service B to be able to fulfill the request.

Error Response

If Service B answers the request with an error, the way in which Service A reacts when answering the client needs to be determined by noting the following:

  • Service A has an API defined by the RAML definition. It is clearly defined what error responses are possible in each situation, and Service A conforms to that definition.
  • The client calls Service A, not aware that it is dependent on nor calls Service B.
  • The error responses defined in the API of Service A are different than the ones defined in the API of Service B.
  • Service A always gets called by a valid tenant, application, and scopes combination as the request passes the proxy, which is not displayed.

Having these things defined, the following propagation rules are defined:

  • Service A does not propagate the response of Service B to the client. A response code not listed in the API of Service A might contain messages with internal-only information.
  • Always respond with a 500 static general error message, because the client cannot fix the problem. An example response is "Something went wrong." Use the 500 error in the following situations:
    • If Service B states that the passed tenant is invalid, then the proxy must be wrong and there is an internal bug or problem.
    • If Service B answers with a 408 Request timeout, Service A retries the request. If the problem persists, use a fallback if available, otherwise, return a 500 error.
    • If Service B is not reachable, such as a 502 Bad Gateway response, use a fallback if available, otherwise, return a 500 error.
    • If Service B is not reachable, such as a 503 Service Unavailable response, use a fallback if available, otherwise, return a 500 error.
    • If Service B is not reachable, such as a 504 Gateway Timeout response, use a fallback if available, otherwise, return a 500 error.
    • If a request to Service B times out like in a socket timeout, use a fallback if available, otherwise, return a 500 error.


Hybris Headers

Every request to the system must pass the API proxy and authenticate using an OAuth2 header. The request is then authenticated and transfers a token into the related authentication details. The tenant, client, user, and scopes related to that request are accessible for the system components and services. This happens by enriching the request with Hybris-specific headers after it enters into the internal system. These headers are present in every service, for any request. The headers are treated in a specific way, as described below.

Definition of hybris headers

NameValueExamplePatternLength
hybris-clientLogical identifier of the calling clientmyexample.myawesomeclient^[a-z][a-z0-9-]{1,14}[a-z0-9][.][a-z][a-z0-9-]{0,22}[a-z0-9]$6-41
hybris-client-idOAuth2 based client identifier of the calling clientC0Kl5Sta1BMiQSqBQyYL5c0yRAwIr94a^.*$-
hybris-client-ownerThe ID of the client's parent tenantmyawesomeproject^[a-z][a-z0-9]+$3-16
hybris-external-pathThe public base path the calling client uses to make the request/hybris/echo/v1^.*$-
hybris-external-urlThe public base URL the calling client uses to make the requesthttps://api.yaas.io/hybris/echo/v1/^.*$-
hybris-hopNumber of hops in the current request chain5^[0-9]+$-
hybris-orgThe identifier of the organization the caller is acting upon59cc4b46f5ce83bf493f7h29^.*$-
hybris-request-idID of the requestAhd7sGAf5K4d^.*$-
hybris-scopesAuthorization scopes granted to the incoming requesthybris.email_send^([a-zA-Z0-9._=-]{1,128}( [a-zA-Z0-9._=-]{1,128})*)?$-
hybris-service-ownerThe ID of the target service's ownermyawesomeproject^[a-z][a-z0-9]+$3-16
hybris-session-idIdentifier used across several requests to identify a session1234^.*$-
hybris-target-urlThe full public URL the calling client uses to make the requesthttps://api.yaas.io/hybris/echo/v1/myresource/1?fields=id^.*$-
hybris-tenantThe ID of the project the caller is acting uponmyexampleshop^[a-z][a-z0-9]+$3-16
hybris-userThe ID of the authenticated usermustermann@hybris.com^.*$-
hybris-user-idThe ID of the authenticated user13e371a2-0189-4753-ab19-c81e39a^.*$-

hybris-client

The hybris-client header identifies the caller. This header is typically in the format <projectIdentifier>.<clientdentifier> where:

  • <projectIdentifier> is the project identifier that the client belong to. It is 3-16 characters long.
  • <clientdentifier> is the local name of the client. It is 2-24 characters long. The header is typically present, and both of these forms are always lowercase.

If the header is not present, and the service requires a header, the service responds with an informative 400 error.

The header might also be in a different format than the one mentioned above.

hybris-client-id

The hybris-client-id header identifies the caller by the OAuth2 client identifier used to authorize the request. In contrast to the value of the hybris-client, the identifier might change over the lifetime of a caller. For example, when the caller requests a new pair of client credentials.

If the header is not present and the service requires a header, the service responds with an informative 400 error.

hybris-client-owner

The hybris-client-owner header identifies the tenant that the caller belongs to. The hybris-client-owner is typically the project ID by which the client is published.

hybris-external-path

The hybris-external-path indicates the original base path that the client application calls. It is the original path that the client uses before the proxy forwards the request. That path can be different than the path a service provides. Any query parameters or fragments are not included.

hybris-external-url

The hybris-external-url indicates the original URL that the client application calls. It is the original URL the client uses, before the proxy forwards the request. With that, the URL is different than the URL a service serves. It does not contain any query parameters or fragments.

hybris-hop

The hybris-hop header is a number identifying the level of depth of the current HTTP request chain. It has diagnostic purposes, and the service includes it as part of any log message. The service propagates it, unaltered, whenever it makes a subsequent request to another YaaS service.

hybris-org

The hybris-org header contains the organization ID the calling service acts upon. The header can be empty or absent, both of which indicate that there is no organization associated with the request.

If the header is not present and the service requires a header, the service responds with an informative 400 error.

hybris-request-id

The hybris-request-id header is a unique identifier of the current HTTP request chain. It has diagnostic purposes, and the service includes it as part of any log message. The service propagates it, unaltered, whenever it makes a subsequent request to another YaaS service.

hybris-scopes

The hybris-scopes header contains a list of authorization scopes granted to the incoming request. Distinct scopes are separated by a single space character and are listed in any order. Each scope has a maximum length of 128 characters, and is usually in the format <org_base_path>.<servicename><scope_name>. Scopes are case-sensitive. If the authorization scopes in the header do not match the required scopes, the service responds with a 403 Unauthorized error.

hybris-service-owner

The hybris-service-owner header identifies the tenant that the called service belongs to. The hybris-service-owner is typically the project ID by which the service is published.

hybris-session-id

The hybris-session-id introduces the concept of a client-driven session by correlating several requests with a single identifier. A client can specify the ID at token aquisition using the oauth2 token endpoint. After the ID is assigned to a token and, with that, to the requests performed together with that token, hybris-session-id indicates the related session ID.

hybris-target-url

The hybris-target-url indicates the full original URL that the client application calls. It is the original URL the client uses, before the proxy forwards the request. With that, the URL is different than the one a service serves. It contains any query parameters or fragments, in contrast to the hybris-external-url header.

hybris-tenant

The hybris-tenant header contains the ID of the tenant that the caller acts upon, such as a project ID. The header is typically present and its value is always in lowercase. If the service makes subsequent requests to other YaaS services, it acts upon same tenant ID and acquires authorization for it. If the header is not present and the service requires a header, the service responds with an informative 400 error.

hybris-user

The hybris-user header contains the identifier for an authenticated user that obtained the OAuth 2.0 access token. In YaaS, the user identifier is typically an email address. The header can be empty or absent, both of which indicate that no authenticated user is associated with the request. The header can contain any character, and its length is restricted only by the general header length. The header value is case-sensitive.

This header is deprecated. Use hybris-user-id instead.

hybris-user-id

The hybris-user-id header contains the identifier for an authenticated user that obtained the OAuth 2.0 access token. The user identifier takes the form of a UUID-generated ID. The header can be empty or absent, both of which indicate that no authenticated user is associated with the request. The header can contain any character, and its length is restricted only by the general header length. The header value is case-sensitive.

RAML modeling

Given a collection resource with a GET method, such as the following:

...
/products:
  get:
    description: Gets all products

To specify that the resource supports Hybris-specific headers, use the public yaas-aware trait covering the most common Hybris-specific headers:

traits:
   - !include https://pattern.yaas.io/v2/trait-yaas-aware.yaml

/products:
  get:
    is: [yaasAware]
    description: Gets all products


Batch Processing

Batch processing is a standardized way of implementing batch operations in REST. The functionality of the /categories/categoryId/productsBatch endpoint enables you to add multiple products to one specific category with a single call. The new endpoint exists in parallel to the /categories/categoryId/products endpoint, which enables you to add one single product to one specific category. Batch-enabled REST endpoints follow a specific naming convention. The names are postfixed with the string Batch, resulting in the following pattern:

/{resource}Batch or /{resource}/{resourceId}/{subresource}Batch

For example, a request looks similar to the following:

Method: POST
Path: /categories/aCategoryId/productsBatch
Body: ['productId1', 'productId2', 'productId3']

The only body carried with the batch request is an array containing single batch items. In the given example, a single batch item is a simple product identifier. But, depending on the scenario, more complex objects are possible. The actual response to a batch request is more complex. For example:

Status: 200
Body: [{
  "status": 200,
  "headers": {
    "location": ["/categories/aCategoryId/products/productId1"]
  }
}, {
  "status": 200,
  "headers": {
    "location": ["/categories/aCategoryId/products/productId2"]
  }
}, {
  "status": 200,
  "headers": {
    "location": ["/categories/aCategoryId/products/productId3"]
  }
}]

A response can be split into two distinct parts:

  • The general HTTP headers describing the overall status.
  • The actual body containing the details for each single batch item.

Each single batch item's response follows the same pattern as defined by the non-batch operation, which helps minimize documentation efforts, increases API consistency, and consequently results in improved API usage simplicity.

API modeling

Each single batch item's response is defined analogous to to the non-batch operation API, seen in the request and response of both carry arrays. The service implementation guarantees that the response for the request array item at position 0 goes into the response array item at position 0. This requirement makes the API easier to use because no unique IDs for each resource need to be contained in either the request or the response.

Request and response items are linked

After processing the result for a request array, the item at position i needs to be written into the response array at position i. The general HTTP headers are influenced by one of two conditions:

  • All single batch operations are successful.
  • A single, multiple, or all batch operations failed.

The following table summarizes which HTTP status code to return:

StatusDescription
200 OKThe batch request was well-formed. Each batch item was processed successfully.
400 Bad RequestThe batch request was malformed. The server did not even start processing single batch items.
403 ForbiddenThe batch request was well-formed but did not contain appropriate credentials. The server did not even start processing single batch items.
500 Internal Server ErrorThe batch request was well-formed, but an unforeseen issue occurred before any kind of processing started.
500 Internal Server ErrorThe server did start processing single batch items but at least one single batch item returned with an error code of 5xx.
400 Bad RequestThe server did start processing single batch items, but at least one single batch item returned with an error code of 4xx and no 5xx error occurred.

A single batch request is not executed in one transaction. Do not expect any kind of rollback functionality, unless explicitly defined in the specific service documentation.


HTTP Caching Support

Enrich responses with Cache-Control headers. There is optional support using conditional caching based on an ETag (Entity Tag) or Last-Modified response headers.

HTTP caching occurs when a client stores local copies of resources for faster retrieval when the resource is requested again. When a service is serving resources, usually triggered by GET requests, it can attach cache headers to the response. These cache headers specify the desired cache behavior.

trait

When an item is fully cached, the client may not contact the service and uses its own cached copy instead. To indicate that a resource is fully cacheable from the service side, it returns the Cache-Control header as part of the GET response. The header turns on the caching from the client side. For instance, when this header is sent and has a value that enables caching, a browser caches the file for as long as specified. Without this header, the browser re-requests the file on each subsequent request. For example, a Cache-Control header value looks similar to the following:

Cache-Control: public, max-age=3600

The public value indicates that the resource can be cached, not only by the end-user's client, but also by any intermediate proxies that serve other users. The max-age value sets a timespan for how long, in seconds, to cache the resource.

While the Cache-Control header turns on client-side caching and sets the max-age value of a resource, the Expires header is used to specify a specific point in time that the resource is no longer valid. To indicate that a resource is fully cacheable with either the max-age value or the Expires header, assign the cacheable trait to your GET method.

trait

Conditional requests are those where the client asks the service if the resource is updated. The client sends information about the cached resource it holds, and the service determines whether the updated resource is returned or if the client's copy is the most recent. If the latter, an HTTP status of 304 (not modified) is returned.

A time-based conditional request is granted only if the requested resource has changed since the client's copy was cached. If the cached copy is the most up-to-date, then the service returns a 304 response code. To enable conditional requests based on time, the service attaches the Last-Modified response header to a response. For example:

Cache-Control: public, max-age=3600
Last-Modified: Fri, 19 Jun 2015 16:17:54 UTC

The next time the client requests this resource, it only asks for the contents of the resource if it is unchanged since the date appearing in the If-Modified-Since request header. For example:

If-Modified-Since: Fri, 19 Jun 2015 16:17:54 UTC

If the resource hasn't changed since Fri, 19 Jun 2015 16:17:54 UTC, the server returns with an empty body and the 304 response code. To indicate that a resource is conditional cacheable based on last modification time, assign the timeCacheable trait to your GET method.

trait

The ETag works in a similar way to the Last-Modified header except its value is a digest of the resource's contents, such as an MD5 hash. The Etag enables the service to identify if the cached contents of the resource are different than the most recent version. For example:

Cache-Control: public, max-age=31536000
ETag: "15f0fff99ed5aae4edffdd6496d7131f"

On subsequent client requests, the If-None-Match request header is sent with the ETag value of the last requested version of the resource. For example:

If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f"

As with the If-Modified-Since header, if the current version has the same ETag value, indicating its value is the same as the client's cached copy, then an HTTP status of 304 is returned. To indicate that a resource is conditional cacheable based on last modification time, assign the contentCacheable trait to your GET method.


Composite Key Resource

In a scenario where the client needs to access the resource directly by the composite key, it becomes part of the URL. For example, when {code} is the business ID of the resource and {app} is a key for the namespacing element of the business ID, the URLs look similar to the following:

POST /{tenant}/resource
GET /{tenant}/resource
GET /{tenant}/resource/{app}
GET /{tenant}/resource/{app}/{code}
PUT /{tenant}/resource/{app}/{code}
DELETE /{tenant}/resource/{app}/{code}

In a scenario where the client does not know the composite key of an entity, the artificial ID approach is used. For example, when {id} is a unique artificial identifier generated at resource creation, the URLs look similar to the following:

POST /{tenant}/resource
GET /{tenant}/resource
GET /{tenant}/resource?app=myApp&code=myCode
GET /{tenant}/resource/{id}
PUT /{tenant}/resource/{id}
DELETE /{tenant}/resource/{id}


Currencies and Units

There are two schemas to express a value of a certain unit described below.

  1. The monetary amount schema: The purpose of the monetary amount schema is to express monetary value, such as a price, in a certain currency. The schema requires that both the amount and the currency attributes are set. The currency should conform to the ISO 4217 standard, such as a JSON object that follows the schema:
    "price" : {
     "amount": 40,
     "currency": "EUR"
    }
    
  2. The quantitative value schema: The quantitative value expresses the amount of items, or the amount of a product in a given unit, such as kilograms or liters. The schema requires that only the value attribute is set. If the unit attribute is missing, it means that the value describes the number of items of the given product. This is equivalent of setting the unit to the value C62.
    "quantity": {
     "value": 1
    }
    
    If the unit attribute is present, it expresses the unit in which the value is given. The value of the unit attribute should be one of the UN/CEFACT Common Codes. Below is an example JSON object that follows the schema:
    "quantity": {
     "value": 1,
     "unit": "KGM"
    }
    

Referring to the JSON schemas

Use the above JSON schemas inside your own custom schemas. There are predefined URLs for the schema definitions:

  1. The monetary amount type
  2. The quantitative value type

Use the related URL in the JSON Schema to define an attribute to be of the monetary amount type:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type" : "object" ,
    "properties": {
        "price": {
            "description": "The price of the product",
            "$ref": "https://pattern.yaas.io/v2/schema-monetary-amount.json"
        },
        ...
   }
}

RAML Modeling

To specify that an endpoint for products in the RAML definition supports these monetized amounts, see the following original product API definition:

...
traits:
  - !include https://pattern.yaas.io/v2/trait-yaas-aware.yaml

resourceTypes:
  - !include https://pattern.yaas.io/v2/resource-type-element.yaml
  - !include https://pattern.yaas.io/v1/resource-type-collection.yaml

/products:
  type: collection
  is: [appAware]
  get:
    description: Gets all products
  post:
    description: Creates a new product

  /{productId}:
    type: element
    is: [appAware]
    get:
      description: Gets a product
    put:
      description: Updates a product
    delete:
      description: Deletes a product

Also, the related product JSON schema:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title":"Product",
  "type":"object",
  "properties":
  {
    "sku":
    {
      "type":"string"
    },
    "name":
    {
      "type":"string"
    },
    "price":
    {
      "type":"number"
    }

  },
  "required":["sku"]
}

To attach a currency to the product price, adjust the product schema by indicating that the price is using the monetary amount schema. For example:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title":"Product",
  "type":"object",
  "properties":
  {
    "sku":
    {
      "type":"string"
    },
    "name":
    {
      "type":"string"
    },
    "price":
    {
      "$ref":"monetaryAmount"
    }
  },
  "required":["sku"]
}

To introduce the monetary amount schema to the RAML file, import the schema-monetary-amount.json schema, with the monetaryAmount name. For example:

...
schemas:
 - monetaryAmount: !include https://pattern.yaas.io/v2/schema-monetary-amount.json

traits:
  - !include https://pattern.yaas.io/v2/trait-yaas-aware.yaml

resourceTypes:
  - !include https://pattern.yaas.io/v2/resource-type-element.yaml
  - !include https://pattern.yaas.io/v1/resource-type-collection.yaml


Date and Time Attributes

As shown in the following audit, shopping, and administration use cases, it is important to maintain the creation and modification time of a resource:

  • A product manager wants to see when a product was last modified.
  • An audit person wants to see when a user was created.
  • A customer wants to see when a product review was created.

Naming date and time attributes

Since the date and time attributes of a resource affects different business roles, the data is part of the response body when querying a resource without the use of headers. The naming of the attributes is always in the following format:

  • A verb in past tense and camel case, plus the word "At", such as: createdAt, modifiedAt, and subscribedAt.
  • The value must be a UTC date and time in ISO 8601 format, such as: yyyy-MM-dd'T'HH:mm:ss.SSSZ. For example:
    "2001-09-11T14:00:00.000Z"
    

A response body looks similar to the following:

Request:
  /products/{sku}

Response:
 Body:
  {
      "sku": "52f56f583004a2d134ddb393",
      "name": "blubb",
      "createdAt": "2014-08-18T15:43:00.000Z",
      "modifiedAt": "2014-08-18T15:44:00.000Z"
  }

RAML modeling

If you have a product schema defined in a JSON file and referenced in a RAML definition, you can specify the product schema definition containing the creation and modification time. The following is an example RAML definition before adding the attributes to the product schema:

...
schemas:
  - product: !include product.json

/products:
  type: collection
  /{id}
    type: element
      get:
        description: Gets product

The following is an example RAML definition after adding the attributes to the product schema:

...
schemas:
  - product: !include product.json
  - createdAt: !include https://pattern.yaas.io/v1/schema-createdAt.json
  - modifiedAt: !include https://pattern.yaas.io/v1/schema-modifiedAt.json

 /products:
  type: collection
  /{id}
    type: element
      get:
        description: Gets product

The following is an example product schema after adding the attributes:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title":"Product",
  "type":"object",
  "properties":
  {
    "sku":
    {
      "type":"string"
    },
    "name":
    {
      "type":"string"
    },
    "createdAt":
    {
      "$ref":"createdAt"
    },
    "modifiedAt":
    {
      "$ref":"modifiedAt"
    }
  },
  "required":["sku"]
}
Do not use "type":"date" in a JSON schema, as this is not supported by the specification. Instead, use "type":"string","format":"date-time".


Localization

The name or description of a product has multiple translations for different languages so that an Italian customer sees different product data than an English customer. Therefore, the client requests product data for a specific language and, optionally, has fallbacks applied in case a certain translation is not available. For instance, English is used as a fallback if the Italian translation is not available. Furthermore, the product data must be retrievable in multiple or all available languages, for example, if you want to display or edit all translations at once. Therefore, a non-existing translation is explicitly indicated instead of applying a fallback. These localized language values are managed in the API of a YaaS service in a specific way. In the following example, the resource Product has a localized attribute name. It first gets retrieved and modified by a front end displaying it in the current language, and then it is retrieved and modified by a back end operating on all available languages.

Front-end retrieval

Always request a Product to have the value of a localized attribute in the best-matching language. Values in other languages are not needed. When calling the GET method without any additional headers, all translations are returned unless the service supports the default language concept. Then, it returns the data in a predefined language. For example, a GET request with no language specified looks similar to the following:

Request:
GET /products/123

Response:
  {
   "name" : "English Name"
  }

To specify a specific language, use the Accept-Language header with the desired language. For example, a GET request with a single language specified looks similar to the following:

Request:
  Accept-Language: pl
  GET /products/123

Response:
  {
   "name" : "Polska nazwa"
  }

To have fallback languages applied, specify a language list using the Accept-Language header, sorted by priority. Optionally, you can specify weightings. As a result, the best-matching language is determined separately for each attribute. A localized product description might be returned in a different language than the product name. There are two types of fallbacks, as described below:

Automatic fallback – Given the Accept-Language header is specified as: en_US. If there are no translations for this specific language within the language family, then the fallback language is en. For example:

Request:
  Accept-Language: en_US
  GET /products/123

Response:
  {
   "name" : "en name translation"
  }

Explicit fallback – If more languages are specified that are not from the same language family, then the fallback language can be explicitly stated. For example:

Request:
  Accept-Language: en, pl
  GET /products/123

Response:
  {
   "name" : "Polska nazwa"
  }

Front-end modification

When working with a storefront, a customer might create or edit a product review. Since this is always done with one language, there is no need to support multiple languages. For instance, modify a product name and call the PUT method without any additional headers. If no translations are available for a localizable field, the request is rejected unless a service implements a default language concept. With the default language ability, the PUT method replaces all the current localizations with the submitted value by default. When the PUT method is partially updated, then the additional language is added to the language map. It is also updated if the language code already exists. For example, a PUT request with no language specified looks similar to the following:

Request:
  PUT /products/123
  {
   "name" : "English Name"  // validation
    or service-specific handling such as assignment to a default language
  }

To specify a specific language, specify the *Content-Language header with the desired language. For example, a PUT request with a single language specified looks similar to the following:

Request:
  Content-Language: pl
  PUT /products/123
  {
   "name" : "English Name"
  }

The same applies for POST requests.

Back-end retrieval

Here, you can operate on multiple languages with no fallbacks ever applied. It is common to pass values in multiple languages within a request and response. To operate on multiple languages, set the hybris-languages header. This indicates the data structure of the body is in a multi-language structure. The attribute type is a mapping of each language to a string instead of just a string. The hybris-languages header specifies a list of languages, or you can specify one language. No language fallback is applied for the header, so the response contains data only in the required language. For example, a GET request with one language specified looks similar to the following:

Request:
  hybris-languages: en
  GET /products/123

Response:
  {
   "name" : { "en": "English name" }
  }

A GET request with a list of languages specified looks similar to the following:

Request:
  hybris-languages: en, pl, it
  GET /products/123

Response:
  {
   "name" : { "en": "English name", "pl": "Polska nazwa", "it": null }
  }

To get the data in all languages, use the asterisk * character for the header value. Do not list the asterisk in combination with any languages, as that is not supported. For example:

Request:
  hybris-languages: *
  GET /products/123

Response:
  {
   "name" : { "en": "English name", "pl": "Polska nazwa" }
  }

Back-end modification

When an administrator updates a product, the language map for a given attribute is provided in the request body. No additional headers are needed. If a PUT request replaces all languages, then all localizations for the provided attribute are replaced. If the request is a partial PUT, the new localized values are added to the localization map, or the values are updated. For example:

Request:
  PUT /products/123
  {
   "name" : { "en": "English name", "pl": "Polska nazwa" }
  }

The example above also works for a POST request that replaces all languages.

RAML modeling

To specify that an endpoint supports localization as described in this document, first look at the original product API definition:

...
traits:
  - !include https://pattern.yaas.io/v2/trait-app-aware.yaml

resourceTypes:
  - !include https://pattern.yaas.io/v2/resource-type-element.yaml
  - !include https://pattern.yaas.io/v1/resource-type-collection.yaml

/products:
  type: collection
  is: [appAware]
  get:
    description: Gets all products
  post:
    description: Creates a new product

  /{productId}:
    type: element
    is: [appAware]
    get:
      description: Gets a product
    put:
      description: Updates a product
    delete:
      description: Deletes a product

Next, look at the related product JSON schema:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title":"Product",
  "type":"object",
  "properties":
  {
    "sku":
    {
      "type":"string"
    },
    "name":
    {
      "type":"string"
    }
  },
  "required":["sku"]
}

To have the product name localized to provide a different value depending on the language, indicate that it is using the localized schema. For example:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title":"Product",
  "type":"object",
  "properties":
  {
    "sku":
    {
      "type":"string"
    },
    "name":
    {
      "$ref":"localized"
    }
  },
  "required":["sku"]
}

Next, import the localized schema, and also define the endpoints as localized. For example:

...
schemas:
 - localized: !include https://pattern.yaas.io/v1/schema-localized.json

traits:
  - !include https://pattern.yaas.io/v1/trait-localized-retrieval.yaml
  - !include https://pattern.yaas.io/v1/trait-localized-modification.yaml
  - !include https://pattern.yaas.io/v2/trait-app-aware.yaml

resourceTypes:
  - !include https://pattern.yaas.io/v2/resource-type-element.yaml
  - !include https://pattern.yaas.io/v1/resource-type-collection.yaml

/products:
  type: collection
  is: [appAware]
  get:
    is: [localizedRetrieval]
    description: Gets all products
  post:
    is: [languageModificationAware]
    description: Creates a new product

  /{productId}:
    type: element
    is: [appAware]
    get:
      is: [localizedRetrieval]
      description: Gets a product
    put:
      is: [languageModificationAware]
      description: Updates a product
    delete:
      description: Deletes a product


Operations

To move items, use action links. For example, to move an item from one wishlist to another:

Request:
  POST /wishlists/1/items/2/moveToWishlist

Response:
  {
    "wishlistId" : 3
  }


Pagination

A collection resource manages a collection of resources, usually retrieved by a GET method. The collection might be too big to return it efficiently, or the whole collection may not be needed at all, and only a subset. It is common practice to provide a way to page through a collection to limit the size of a response, and request only a subset, or page, explicitly.

To specify a paged request, specify the maximal size of the response with the pageSize parameter, and the subset number with the pageNumber parameter:

Request:
GET /products?pageNumber=2&pageSize=30

Response:
 Headers:
    Link: <http://foo/?page=2>; rel='self'
    Link: <http://foo/?page=3>; rel='next'
    Link: <http://foo/?page=1>; rel='prev'

 Body:
  [
    {
        "sku": "52f56f583004a2d134ddb393",
    },
    {
        "sku": "52f56f583004a2d134ddb394",
    },
    ...
]

The call does not fail if the pageNumber or pageSize parameters are not defined for a given request. Instead, default values are applied that are specific for the domain and do not cause performance issues. When marking a method as paged, provide paging information as part of the response. This is not specified in detail yet. The call returns a 400 response code if the arguments are syntactically incorrect. For example, if the pageNumber or pageSize values are negative or non-numeric. If the call is successful, a 200 response is returned with a Link header that includes a link to the current page, the next page, and the previous page.

The pages are marked accordingly with rel self, rel next, and rel prev. The link to the current page self is required and must be provided. The link to the next page next is optional. If not present, the next page is not available, and the current page is the last page of the result. The link to the previous page prev is also optional. If not present, the previous page is not available, and the current page is the first page of the result.

RAML modeling

Given a collection resource with a GET method, such as the following:

...
/products:
  type: collection
  get:
    description: Gets all products
  post:
    description: Creates a new product

Specify the GET method as pageable by using the header, such as this public pattern already provided:

traits:
   - !include https://pattern.yaas.io/v2/trait-paged.yaml

/products:
  type: collection
  get:
    is: [paged]
    description: Gets all products
  post:
    description: Creates a new product


Partial Update

With the standard HTTP methods, the only way to update a resource is by using the PUT method. This usage is idempotent, and replaces the resource with the entity specified in the payload of the PUT request.

There are scenarios when a replacement of the resource is not desired, nor efficient, such as when you GET a minimal view of a resource, because only the minimal attribute set is required to display. After attributes are modified, you apply the changes. In theory, you GET the full resource data first, apply your modifications, and then PUT the modified resource. Another scenario is when a cart resource contains existing items and you want to add an item to the cart without having to send the entire cart, just to add one item.

A solution is to use the PATCH method for applying a partial update. Unfortunately, this HTTP method is not part of the HTTP specification. Instead, a dedicated specification is defined for the PATCH method. Since it is a well-defined standard but not covered by the current HTTP specification, not all software accepts it. However, big API providers use it already, and it is a better solution than overloading other methods with additional merge semantics.

YaaS API's should use the PATCH method to apply partial updates to an existing resource. The default media type to accept should be the JSON merge patch format: application/merge-patch+json. However, an API should be tolerant by accepting requests annotated with the format: application/json.

The PATCH method specifies the following:

  • Apply a set of changes described in the request entity to the resource identified by the Request-URI.
  • If the Request-URI does not point to an existing resource, the server may create a new resource.
  • A PATCH request can be issued in an idempotent way, which helps prevent bad outcomes from collisions between two PATCH requests on the same resource in a similar time frame.
  • The server must apply the entire set of changes atomically, and it never provides a partially-modified representation. If the entire patch document cannot be successfully applied, the server must not apply any of the changes.

The JSON merge patch format defines rules for merging a given JSON document to an existing server-side resource. The rules are simplified as:

  • Include the properties to be updated with their new values.
  • Don't include properties that are not to be updated.
  • Set properties to be 'deleted' to null.

Given a product resource with an ID of 123, such as the following:

{
   "id": "123",
   "color": "blue",
   "taste": "bitter",
   "reviews": [
     {"rating": 4, "comment":"great"},
     {"rating": 3, "comment":"average thingy"}
   ],
   "dimensions":{
     "height": 6,
     "width": 7
   },
   "size": 4
}

The results of the request looks similar to the following:

Request:
PATCH /products/123
  Content-Type: application/merge-patch+json
  {
     "color": null,
     "reviews": [
       {"rating": 4, "comment":"great"}
     ],
     "dimensions":{
       "width": 2
     },
    „size": 5
  }

Resulting state:
  {
   "id": "123",
   "taste": "bitter",
   "reviews": [
     {"rating": 4, "comment":"great"}
   ],
   "dimensions":{
     "height": 6,
     "width": 2
   },
   "size": 5
  }

RAML Modeling

Given a product resource, such as the following:

...
 resourceTypes:
   - !include https://pattern.yaas.io/v3/resource-type-element.yaml
...
  /{productId}:
    type: element
    is: [appAware]
    get:
      description: Gets a product
    put:
      description: Replaces a product
    delete:
      description: Deletes a product

To support partial updates, add the PATCH method to your API resource in the RAML definition. For example:

...
 resourceTypes:
   - !include https://pattern.yaas.io/v3/resource-type-element.yaml

...
  /{productId}:
    type: element
    get:
      description: Gets a product
    put:
      description: Replaces a product
    patch:
      description: Updates a product
    delete:
      description: Deletes a product

See the resource type definition on how to define a PATCH method in detail.


Resource Identifier

The nature of an API is that it is adapted to changing requirements, and evolves over time. Resources such as data objects, files, and database entities will probably change their structure during their lifetime as well. For this reason, semantic versioning of APIs is used so that a version number reflects the actual status of the API at any certain point in time. But identifying a certain resource or data object to access it should be independent of the API version, because it provides the information about the current schema, or format of a entity. For example, identifying a customer by its ID inside a database should be independent from the used database schemata.

A Uniform Resource Locator (URL) is a subset of the Uniform Resource Identifier (URI) that specifies where an identified resource is available, and the mechanism for retrieving it. Using a URL of a YaaS API, such as https://api.yaas.io/hybris/account/v1/projects/myProject always implies the version of that API to identify a resource at later point in time. To persist the resource identifier in version-independent fashion, the resource type and resource identifier should not be saved as a URL.

In contrast, a Uniform Resource Name (URN) is a Uniform Resource Identifier (URI) that uses the URN schema, and does not imply availability of the identified resource. Therefore, a URN identifies a resource by name in a given namespace, but does not define how the resource is obtained. Both URNs (names) and URLs (locators) are URIs.

URL Examples:

- http://www.ietf.org/rfc/rfc2396.txt
- ldap://[2001:db8::7]/c=GB?objectClass?one
- mailto:John.Doe@example.com
- telnet://192.0.2.16:80/

URN Examples:

- urn:oasis:names:specification:docbook:dtd:xml:4.1.2
- urn:isbn:0451450523
- urn:ISSN:0167-6423
- urn:ietf:rfc:2648
- urn:mpeg:mpeg7:schema:2001
- urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66

There are two ways to identify a resource in the YaaS API in a versionless way. Depending on the context, you either identify a resource locally within its related resource type, such as specifying a product amongst other products using its code, or globally amongst all resource types. Both scenarios are described below with a recommendation on how to handle resource identification without versions.

Local resource identifier

A resource in the YaaS API usually is identified by an attribute containing an artificial or composed identification value that is unique among all resources of that resource type. For example, the id attribute of a project in the account service, is unique amongst all project resources of the account service. An identifier attribute that uniquely identifies a resource in the context of a resource type, is called a local resource identifier. It ensures that the resource has uniqueness in the context of a specific resource type. If the identifier is artificial, then the related attribute in a payload is always id. For example:

"product" :
{
  "id" : "12",
  "name" : "shoe"
}

Global YaaS resource name (YRN)

In YaaS, a global resource identifier is a Uniform Resource Name (URN) with a custom YaaS schema and is called a YaaS Resource Name (YRN). The URN namespace used by YaaS is identified by yaas, and is composed of the resource type and the local resource identifier in the following way:

<URN> ::= "urn:yaas:" <RESOURCE_TYPE> ":" <RESOURCE_IDENTIFIER>
<RESOURCE_TYPE> ::= <ORGANIZATION> ":" <SERVICE> ":" <RESOURCE>
<RESOURCE_IDENTIFIER> ::=  <ID_PART> { ";" <ID_PART> }
<ORGANIZATION> ::= <TEXT>
<SERVICE> ::= <TEXT>
<RESOURCE> ::= <TEXT>
<ID_PART> ::= <TEXT>
<TEXT> ::= [A-Za-z0-9-_.~]*

Or, expressed more concise:

urn:yaas:<organization>:<service>:<resource>:<id_part>{;<id_part>}

In the definition above, TEXT must be URL-encoded to ensure that it fits to the following specified pattern:

  • organization – The organization base path used by the service owning the resource type, such as hybris in: https://api.yaas.io/hybris/account/v1.
  • service – The identifier of the service owning the resource type, such as account in: https://api.yaas.io/hybris/account/v1.
  • resource – The identifier of the resource type to which the resource belongs. Usually it's the singular form of the resource base path, such as project in: https://api.yaas.io/hybris/account/v1/projects/{id}. For a nested resource, the parent resource type can be used as a prefix separated by a hyphen, such as product-variant in: https://api.yaas.io/hybris/product/v1/products/{id}/variants/{id}. The usage of a prefix will not imply any structure on the id_part.
  • id_part – The local resource identifier, which can be multiples, such as with composite keys, or when belonging to a tenant like with myshop and 67 in https://api.yaas.io/hybris/product/v1/myshop/products/67.

Examples of resource URL's and the resulting YRN

URLYRNComment
https://api.yaas.io/hybris/account/v1/projects/myshopurn:yaas:hybris:account:project:myshopNo tenant related to a project.
https://api.yaas.io/hybris/product/v1/myshop/products/56urn:yaas:hybris:product:product:myshop;56A product is specific, per tenant.
https://api.yaas.io/hybris/cart/v1/myshop/carts/56/items/12urn:yaas:hybris:cart:cart-item:myshop;56;12Sub-resource items have a unique resource type identifier cart-items assigned, as defined in the RAML definition of the cart service. The composite key lists the tenant, the cart, and the cart item.

General guidelines

  • When a resource is existent per tenant, the tenant must be part of the composite key.
  • The resource type identifier itself is not registered anywhere and must be looked up in the RAML definition of the corresponding service. A resource type identifier should be specified for any resource base path specified.
  • The composite ID and URN must be looked up in the RAML definition of the corresponding service. The composite IDs should be specified for any resource base path specified.

YaaS resource name creation

An example YRN which leverages the placeholders used in the resource definitions should be specified in the RAML definition of the related service managing that resource. For example, a cart resource has a URN specification in the RAML file that looks similar to the following:

/{tenant}/carts/{cartId}/items/{itemId}:
  description: |
    My description text.
    YRN: urn:yaas:hybris:cart:cart-item:{tenant};{cartId};{itemId}
  get:
    ...

Do not construct a YRN manually, as every resource in YaaS should have the option to include the YRN as part of the response payload. For example:

GET /myshop/products/12

"product":{
  "yrn":"urn:yaas:hybris:product:product:myshop;12",
  "id":"12",
  "name":"shoe"
}

The attribute type for the payload looks similar to the following:

{
      "$schema": "http://json-schema.org/draft-04/schema#",
      "title":"YRN attribute",
      "type": "string",
      "format": "URI",
      "description": "Contains the YaaS Resource Name (YRN) of a specific resource on base of the 'yaas' URN scheme"
}

YaaS resource name resolution

You can transform a URN into a URL at any time by using the URL template of the related resource type. There is no dynamic resource type registry yet. Build a manual mapping registry inside your service instead. For example:

hybris:account:project=https://api.yaas.io/hybris/account/v1/projects/{0}
hybris:product:product=https://api.yaas.io/hybris/product/v1/{0}/products/{1}
hybris:cart:cart-item=https://api.yaas.io/hybris/cart/v1/{0}/carts/{1}/items/{2}

You can then parse a given YRN by using the regular expression:

urn:yaas:(?P<res_type>[A-Za-z0-9-_.~]+:[A-Za-z0-9-_.~]+:[A-Za-z0-9-_.~]+):(?P<res_id>[A-Za-z0-9-_.~]+)

Look up the related URL in the template by the parsed res_type group. The res_id group is split by the ; character, and filled into the resolved URL template. For example, given the following YRN: urn:yaas:hybris:product:product:myshop;12 and the example URL template mapping mentioned above, the regular expression results in two groups:

res_type=hybris:product:product
res_id=myshop;12

With the help of the res_type, you can resolve the URL template https://api.yaas.io/hybris/product/v1/{0}/products/{1}, split the res_id into pieces, and fill-in the template: https://api.yaas.io/hybris/product/v1/myshop/products/12.

Java support

If you use Java and Jackson for marshalling JSON files, you can marshal YRNs directly into a POJO using the following utility class:

@JsonSerialize(converter = YRNSerializer.class)
public class YRN
{
    private static final String URN_SEP = ":";
    private static final String URN_GROUP_RESOURCE = "resource";
    private static final String URN_GROUP_ID = "rid";

    /** The regexp to resolve resource type and id from a YRN. */
    private static final Pattern URN_PATTERN = Pattern
            .compile("urn" + URN_SEP + "yaas" + URN_SEP + "(?<" + URN_GROUP_RESOURCE + ">[a-zA-Z]+" + URN_SEP
                    + "[a-zA-Z]+" + URN_SEP + "[a-zA-Z-]+)" + URN_SEP + "(?<" + URN_GROUP_ID + ">.+)");

    private final String resource;
    private final String[] ids;
    private final URI urn;

    private YRN(final String resource, final String[] ids)
    {
        this.resource = resource;
        this.ids = Arrays.copyOf(ids, ids.length);
        this.urn = constructURN(resource, ids);
    }

    private static URI constructURN(final String resource, final String[] ids)
    {
        final StringBuilder urnBuilder = new StringBuilder();
        urnBuilder.append("urn").append(URN_SEP).append("yaas").append(URN_SEP);
        urnBuilder.append(resource).append(URN_SEP);
        urnBuilder.append(String.join(";", ids));
        return URI.create(urnBuilder.toString());
    }

    public String getResource()
    {
        return resource;
    }

    public String getId(final int part)
    {
        return ids[part];
    }

    public String[] getIds()
    {
        return ids;
    }

    public URI toURI()
    {
        return urn;
    }

    /** Method to convert a String into a URN. Used by Jackson automatically. */
    public static YRN valueOf(final String urn)
    {
        final Matcher matcher = URN_PATTERN.matcher(urn);
        if (!matcher.matches())
        {
            throw new IllegalArgumentException("Given URN '" + urn + "' does not match yaas urn scheme");
        }

        final String resource = matcher.group(URN_GROUP_RESOURCE);
        final String idRaw = matcher.group(URN_GROUP_ID);
        return new YRN(resource, idRaw.split(";"));
    }

    public static YRNBuilder of(final String resource)
    {
        return new YRNBuilder(resource);
    }

    @Override
    public String toString()
    {
        return this.urn.toString();
    }

    public static class YRNBuilder
    {
        private final String resource;
        private final List<String> ids;

        public YRNBuilder(final String resource)
        {
            this.resource = resource;
            this.ids = new ArrayList<>();
        }

        public YRNBuilder withId(final String newId)
        {
            this.ids.add(newId);
            return this;
        }

        public YRN build()
        {
            return new YRN(resource, ids.toArray(new String[ids.size()]));
        }
    }

    /** A Jackson based converter for converting a YRN into a String. */
    static class YRNSerializer extends StdConverter<YRN, String>
    {
        public YRNSerializer()
        {
            super();
        }

        @Override
        public String convert(final YRN value)
        {
            if (value == null)
            {
                return null;
            }
            return value.toString();
        }
    }
}

Use this utility class in your POJO as an attribute type, and Jackson marshalls it automatically:

public class Product
{
    private String id;
    private YaasURN urn;

    public YaasURN getUrn()
    {
        return urn;
    }

    public void setUrn(final YaasURN urn)
    {
        this.urn = urn;
    }

    public String getId()
    {
        return id;
    }

    public void setId(final String id)
    {
        this.id = id;
    }
}

To finish resolving a YRN to a URL, you need a way to map the resource part of a YRN to the related URL template. This is done in a convenient way by defining the mapping itself as properties. For example:

yrn.hybris:product:product=https://api.yaas.io/hybris/product/v1/{0}/products/{1}

Use a convenience mechanism to load properties, such as Spring annotations, and use Spring's URITemplate to map a URN to a URL:

@Configuration
@ConfigurationProperties(prefix = "yrn")
public class YRNMapper
{
    private Map<String, String> mappings = new HashMap<>();

    public Map<String, String> getMappings()
    {
        return mappings;
    }

    public void setMappings(final Map<String, String> mappings)
    {
        this.mappings = mappings;
    }

    private UriTemplate resolveURLTemplate(final String resource)
    {
        final String entry = mappings.get(resource);
        if (entry == null)
        {
            throw new IllegalArgumentException("Cannot find a URL template for " + resource);
        }
        return new UriTemplate(entry);
    }

    public URI resolveURL(final YaasURN urn)
    {
        return resolveURLTemplate(urn.getResource()).expand((Object[]) urn.getIds());
    }
}


Resource Reference

There are different ways to reference resources in payloads, as described in this document. Most often you find referenced resources that are managed by the same service. They imply the kind of resource they are by the attribute name, such as "product":"5". Sometimes, more general references are used, such as references to items in a cart, and you cannot imply the resource type easily.

Typed local references

When referencing a resource managed by the same service, usually the referenced resource type uses an explicit attribute name. For example:

"wishlist" :
{
  "id" : "5",
  "wishlistItems" : ["34","67"]
}

The referenced resources have an explicit attribute name of wishlist items. If these wishlist items are a resource of the same service, the association is automatically explicit. This scenario is sufficient in most cases, however, the referenced resource type should be specified in the related RAML definition, and always be explicit as possible. For example:

"properties" :
{
  "wishlistItems" :
  {
     "type" : "string",
     "description" : "array of 'wishlistItem' resources managed by the 'wishlist' service identified by 'id'.
  }
}

Typed global references

When referencing resources with well-formulated attribute names not managed by the same service, then you usually use an attribute name explicitly indicating the referenced resource type. For example:

"wishlist" :
{
  "id" : "5",
  "products" : ["34","67"]
}

Because it is not clear where the referenced resource can be found, you must specify the referenced resource in the RAML definition. For example:

"properties" :
{
  "products" :
  {
     "type" : "string",
     "description" : "array of 'product' resources managed by the 'product' service identified by 'code'.
  }
}

It is expected that the products referenced by the products attribute are managed by the SAP Hybris Products service and the references are resolved within that service. However, to be explicit, you could use the [YRN](#ResourceIdentifier) to establish the references. For example:

"wishlist" :
{
  "id" : "5",
  "products" : ["urn:yaas:hybris:product:product:myShop;34","urn:yaas:hybris:product:product:myShop;67"]
}

The referenced resource turns into a generic reference that the user can interpret, so products listed here could be managed by different product services.

Untyped references

In some scenarios, you want to establish references where the referenced resource type can not be fixed, because it can be of different types. For example, given an authorization service with authorization rules assigned to the resources:

"authorizationRule" :
{
  "id" : "5",
  "resource" : "XX"
}

The definition does not specify what resource type is referenced, by design. All the information must be encoded in the reference value. For this scenario, the [YRN](#ResourceIdentifier) is the perfect choice and must be used because it contains all the required data in a concise way. For example:

"authorizationRule" :
{
  "id" : "5",
  "resource" : "urn:yaas:hybris:product:product:myShop;67"
}


Response Filtering

The API of an SAP Hybris service is designed to meet the most common requirements. Therefore, the payload of a response might not fit every use case because it contains too much data. A single resource entity contains many data fields, and the horizontal filtering is addressed, but the payload still contains too many entities.

A common approach is to limit the attributes returned for a single entity by applying some kind of filter specifying only the attributes of interest. Filtering can highly decrease network traffic, as well as decrease server and client-side marshalling and unmarshalling of data.

To specify a filter to influence the size of a single entity, provide the field's query parameter. It specifies the attributes as a comma-separated list that an entity must contain. For example, a product with code, name, and description fields can have a filter applied in the following manner:

Request:
GET /products/123?fields=code

Response:
  {
    "code": "52f56f583004a2d134ddb393"
  }

Another request having a list of fields specified looks similar to the following:

Request:
GET /products/123?fields=code,name

Response:
  {
    "code": "52f56f583004a2d134ddb393",
    "name": "fancy shoe"
  }

A request returns a response code of 400 if the value of the limited request attribute is not provided or is empty. The request does not fail when limiting to non-existing fields. Instead, it returns a minimal, essential set of fields to identify the entities clearly, such as the ID.

RAML modeling

Given a collection resource with a GET method, such as the following:

...
/products:
  type: collection
  get:
    description: Gets all products

Specify the GET method as filterable by applying a query parameter, such as this public pattern already provided:

traits:
   - !include http://pattern.yaas.io/v1/trait-limited.yaml

/products:
  type: collection
  get:
    is: [limited]
    description: Gets all products


Searching

When calling a GET on a collection resource, all entities provided by this endpoint are returned. Limit the size of the result list by filtering the entities with the attribute criteria.

To specify the attribute criteria for a method returning a collection of entities, provide the q query parameter. This parameter specifies the criteria for attributes that the entities must match to be selected. The value is an ElasticSearch simple query. For example, a product with a name and description for the sku* field can have a search criteria applied in the following manner:

Request:
GET /products?q=name:"Fancy Shoe"

Response:
  {
    [
      {
       "sku": "1",
       "name": "Fancy Shoe",
       "size": "5"
      },
      {
       "sku": "2",
       "name": "Fancy Shoe",
       "size": "3"
      }
    ]
  }
Request:
GET /products?q=name:"Fancy Shoe" size<4

Response:
  {
    [
      {
       "sku": "2",
       "name": "Fancy Shoe",
       "size": "3"
      }
    ]
  }

Requests return a 400 error code if the given criteria is not translated into a proper query. Provide detailed explanations in the error payload on how to fix the query itself. The explanations are also consistent across all APIs.

RAML modeling

Given a collection resource with a GET method, such as the following:

...
/products:
  type: collection
  get:
    description: Gets all products

Specify the GET method as searchable by applying a query parameter, such as this public pattern that is already provided:

traits:
   - !include https://pattern.yaas.io/v1/trait-queryable.yaml

/products:
  type: collection
  get:
    is: [queryable]
    description: Gets all products


Sorting

When calling a GET on a collection resource, a list of entities is returned, but the order of the collection is not defined by default. For an option to guarantee the order, specify an order criteria.

To specify an order criteria for a method returning a collection of entities, provide the sort query parameter. It specifies the attributes to sort as a comma-separated list. The default order direction is ascending, but this can be changed with the desc sort criteria. For example, a product with a name and description for the sku field can have a sort criteria applied in the following manner:

Request:
GET /products?sort=name

Response:
  {
    [
      {
       "sku": "2",
       "name": "a",
       "description": "text",
      },
      {
       "sku": "1",
       "name": "b",
       "description": "text"
      }
    ]
  }
Request:
GET /products?sort=description,name

Response:
  {
    [
      {
       "sku": "2",
       "name": "a",
       "description": "text",
      },
      {
       "sku": "1",
       "name": "b",
       "description": "text"
      }
    ]
  }
Request:
GET /products?sort=sku:desc,name

Response:
  {
    [
      {
       "sku": "2",
       "name": "a",
       "description": "text",
      },
      {
       "sku": "1",
       "name": "b",
       "description": "text"
      }
    ]
  }

The call ignores non-existing fields, as opposed to failing. Sorting for the same kind of data, such as literal, numeric, and domain specific types, works consistently across the API. For example, dates are not ordered the same way as textual fields. Sort the final result starting with the first column and ending with the last column, with decreasing priority.

RAML modeling

Given a collection resource with a GET method, such as the following:

...
/products:
  type: collection
  get:
    description: Gets all products

Specify the GET method as sortable by applying a query parameter, such as this public pattern already provided:

traits:
   - !include https://pattern.yaas.io/v1/trait-sortable.yaml

/products:
  type: collection
  get:
    is: [sortable]
    description: Gets all products


  • Send feedback

    If you find any information that is unclear or incorrect, please let us know so that we can improve the Dev Portal content.

  • Get Help

    Use our private help channel. Receive updates over email and contact our specialists directly.

  • hybris Experts

    If you need more information about this topic, visit hybris Experts to post your own question and interact with our community and experts.