Skip to content

allegro/restapi-guideline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Allegro REST API Design Guidelines

The purpose of this document is to keep Allegro Public REST API implementations as consistent as possible. The document is addressed to developers who:

  • design REST APIs for micro-services
  • create applications that consume these APIs.

These guidelines apply to all REST APIs, in particular:

  • REST APIs that will provide resources exposed in Allegro REST API
  • other REST APIs that will be consumed by Allegro Mobile Applications

We publish this document in order to:

  • help you to understand the principles behind our REST API design if you are a developer/administrator who wants to interact with the API provided by Allegro Public REST API or micro-services,
  • ensure consistency between your service and the Allegro REST API if you are developing a service consuming a REST API,
  • hear your feedback.

Allegro REST API

Allegro REST API is a new API interface that will integrate the Allegro platform with:

  • third-party applications that use Allegro WebAPI at present,
  • Allegro Mobile Applications,
  • frontend / web applications.

We are providing you with Allegro REST API, while we try to deliver truly RESTful API interface with positive developer experience (DX).

Resources that will contribute to Allegro REST API will be provided by micro-services.

Each micro-service will provide its own REST API resources exposing the Allegro platform's specific functionality. The goal of these application-specific APIs is to share common design standards, as described herein.

What is RESTful API interface?

The REST architectural style describes six constraints. These constraints, applied to the architecture, were originally communicated by Roy Fielding in his doctoral dissertation (see http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) and defines the basis of RESTful-style.

The six constraints are:

The uniform interface constraint is fundamental to the design of any REST service. The uniform interface simplifies and decouples the architecture, which enables each part to evolve independently.

The four constraints concerning uniform interface are:

Resource-Based

Individual resources are identified in requests, for example using URIs in web-based REST systems. The resources themselves are conceptually separate from the representations that are returned to the client. For example, the server may send data from its database as HTML, XML or JSON, none of which are the server's internal representation.

Manipulation of resources through these representations

When a client holds a representation of a resource, including any metadata attached, it has enough information to modify or delete the resource on the server, provided it has permission to do so.

Self-descriptive messages

Each message includes enough information to describe how to process the message. For example, which parser to invoke may be specified by an Internet media type (previously known as a MIME type). Responses also explicitly indicate their cache-ability.

Hypermedia as the engine of application state (HATEOAS)

Clients deliver state via body contents, query-string parameters, request headers and the requested URI (the resource name). Services deliver state to clients via body content, response codes, and response headers. This is technically referred to as hypermedia (or hyperlinks within hypertext). Aside from the description above, HATEOS also means that, where necessary, links are contained in the returned body (or headers) to supply the URI for retrieval of the object itself or related objects. We will talk about this in more detail later.

If a service violates any of the required constraints, it cannot be considered RESTful.

Complying with these constraints, and thus conforming to the REST architectural style, enables any kind of distributed hypermedia system to have desirable non-functional properties such as performance, scalability, simplicity, modifiability, visibility, portability, and reliability.

What is Developer Experience?

One of the key principles of good API design is that an interface must provide a seamless and user-friendly developer experience (DX) if it is to facilitate the creation of applications that add value to the API owner’s business.

Contents

Resource

Name

Use the plural version of a resource name to be consistent when referring to particular resources, e.g.:

/users
/offers
/contests
/shipments
/transactions
/payments

Identification (UU)IDs

Use UUIDs unless you have a very good reason not to. Do not use IDs that will not be globally unique across instances of the service or other resources in the service, especially auto-incrementing IDs.

Provide UUIDs as a lowercase string in 8-4-4-4-12 format, e.g.:

01234567-89ab-cdef-0123-456789abcdef

Lowercase paths

Use lowercase and dash-separated path names, e.g.:

/general-deliveries
/application-configurations

Minimize resources nesting

In data models with nested parent/child resource relationships, paths may become deeply nested, e.g.:

/users/{userId}/offers/{offerId}/shipments/{shipmentId}

Limit nesting depth by preferring to locate resources at the root path. Use nesting to indicate scoped collections. For example, for the case above where shipping belongs to an offer:

/users/{userId}
/users/{userId}/offers
/offers/{offerId}
/offers/{offerId}/shipments
/shipments/{shipmentId}

Versioning

To specify version use custom media type and +json Structured Syntax Suffix i.e. application/vnd.allegro.public.v1+json. Each request must specify its version by setting up Accept or Content-type properly. The header depends on method semantics e.g.: GET methods usually expect Accept header, POST methods usually expect Content-type header and PUT methods usually expect both headers.

Content-Type: application/vnd.allegro.public.v1+json
Accept: application/vnd.allegro.public.v1+json

With jersey you can simply use:

@Consumes("application/vnd.allegro.public.v1+json")
@Produces("application/vnd.allegro.public.v1+json")

We don't introduce new API versions, all changes also those that are breaking would be introduced under already existing version, marked as application/vnd.allegro.public.v1+json.

Instead of API versioning, we have the procedures to handle new changes depending on their type. The context, decision, and types of changes and how we handle them are described in document ADR-001: Public API is not versioned.

Beta resources

Beta version of API resources is aimed at helping developers get familiar with it before releasing a new API resource. Beta version resources can change continuously and affect compatibility of current API.

Beta version resources are marked the same way as described above, but “public” is replaced with “beta” in a media type, e.g. application/vnd.allegro.beta.v1+json.

Content-Type: application/vnd.allegro.beta.v1+json
Accept: application/vnd.allegro.beta.v1+json

Rules for new beta resources:

  • within a given version, beta resources can be subject to modifications. As a result, we do not recommend the production use of beta resources,
  • breaking changes within the beta version can be implemented within subsequent versions and marked, for example, as application/vnd.allegro.beta.v2+json,
  • if you complete your work with the beta version, release it as application/vnd.allegro.public.v1+json.

Beta version marked as application/vnd.allegro.beta.v1+json can be used in exceptional situations to implement compatibility-breaking changes to resources that have already been released in the version marked as application/vnd.allegro.public.v1+json. Read more on that in ADR-001: Public API is not versioned.

Views

If you need different views of the same object, construct different URLs to distinguish views.

For example: some offer data are available only for seller. To get these private data, add context /sale/ to the path:

GET /sale/offers

To get only public data use:

GET /offers

To get offers watched by user:

GET /watched-offers

User Agent Header

All API requests MUST include a valid User-Agent header.

Here are default User-Agent examples:

User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Iron/35.0.1900.0 Chrome/35.0.1900.0 Safari/537.36
User-Agent: Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1

Identifying your client

When you make HTTP requests to the API, be sure to specify a User-Agent header that properly identifies your client. Make up a custom value of an User-Agent header that identifies your service or application.

Here are examples of custom User-Agent headers:

For mobile application:

User-Agent: Application-Package-Name_BundleId/ApplicationVersion (Client-Id CMDeviceUUID) OSName/OSVersion (Manufacturer Model)

For a service:

User-Agent: Your-Service-Name/ServiceVersion (your-service-host-id) OSName

Sample:

User-Agent: pl.allegro.sale/1.5.0 (Client-Id 01234567-89ab-cdef-0123-456789abcdef) Android/4.0 (Motorola XT1052)

Use HTTP methods to operate on collections and entities

There is one single rule concerning operations performed on collections and entities - Use HTTP methods

You can operate on resources using HTTP methods such as POST, GET, PUT, and DELETE - to remember them, refer to the CRUD acronym (Create-Read-Update-Delete).

Resource / HTTP method POST (create) GET (read) PUT (update) DELETE (delete)
/users Create new user List users Error Error
/users/{uuid} Error Get user Update user if exists Delete user

Update and create must return a resource representation

PUT and POST methods modify fields of the underlying resource that were not part of the provided parameters (for example: createdAt or updatedAt timestamps). As indicated in the section Provide full resources where available, to prevent an API consumer from having to hit the API again for an updated representation, have the API return the updated (or created) representation as part of the response.

Create entity

  • server requires all entity properties to be located in a request body
  • server returns the HTTP 201 Created status code
  • response will also include the Location header that points to the URL of the new resource (RFC 6570)
  • server returns the new entity in the established version (see Versioning section)

Sample request:

curl -X POST https://api.allegro.pl/users \
    -H "Accept: application/vnd.allegro.public.v1+json" \
    -H "Content-Type: application/vnd.allegro.public.v1+json" \
    -d
{
  "firstName": "John",
  "middleName": "Doe",
  "nickName": "Zen",
  "email": "zed@allegro.pl"
}

Sample response:

  • status: 201 Created
  • header Location: https://api.allegro.pl/users/01234567-89ab-cdef-0123-456789abcdef
  • header Content-Type: application/vnd.allegro.public.v1+json
  • whole entity is included in a body (additional fields such as id, createdAt, updatedAt)
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "firstName": "John",
  "middleName": "Doe",
  "nickName": "Zen",
  "email": "zed@allegro.pl",
  "createdAt": "2012-01-01T12:00:00.000Z",
  "updatedAt": "2012-01-01T13:00:00.000Z"
}

List entities (collection)

  • server returns the HTTP 200 Ok status code
  • response collection is presented as an array (in the established version) in a field bearing the same name as the resource (Wrap collection in object)

Sample request:

curl https://api.allegro.pl/users -H "Accept: application/vnd.allegro.public.v1+json"

Get selected entity

  • server returns the HTTP 200 Ok status code
  • server returns an entity in the established version

Sample request:

curl https://api.allegro.pl/users/{id} -H "Accept: application/vnd.allegro.public.v1+json"

Update entity

  • to update an entity, the server requires being provided with all the properties that can be subject to update
  • server returns the HTTP 200 Ok status code
  • server returns an updated entity in the established version

Sample request:

curl -X PUT https://api.allegro.pl/users/01234567-89ab-cdef-0123-456789abcdef \
    -H "Accept: application/vnd.allegro.public.v1+json" \
    -H "Content-Type: application/vnd.allegro.public.v1+json" \
    -d
{
  "middleName": null,
  "nickName": "Zed"
}

Sample response:

  • status: 200 Ok
  • header Content-Type: application/vnd.allegro.public.v1+json
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "firstName": "John",
  "middleName": null,
  "nickName": "Zed",
  "email": "zed@allegro.pl",
  "createdAt": "2012-01-01T12:00:00.000Z",
  "updatedAt": "2012-01-01T13:00:00.000Z"
}

In the example above, a nick name is changed to "Zed" and a middle name is left empty (assuming that the field middleName is optional).

Delete entity

  • server returns the HTTP 204 No Content status code
  • response body content is empty
  • no need for Accept header (outcome of the Delete method should not be versioned, except for special cases – see Versioning section)

Sample request:

curl -X DELETE https://api.allegro.pl/users/{id}

Return appropriate status codes

HTTP Status Codes

Return appropriate HTTP status codes with each response. Responses concerning successful operations should be coded according to the following rules:

  • 200 Ok: Response to a successful GET, PUT, PATCH request. An existing resource is updated synchronously.
  • 201 Created: Response to a POST request that results in entity creation. It should be combined with a Location header pointing to the location of the new resource.
  • 202 Accepted: Accepted request concerning a POST, PUT, DELETE, or PATCH request that will be processed asynchronously.
  • 204 No Content: Response to a successful request that will return empty body (as a DELETE request)
  • 304 Not Modified: Used when HTTP caching headers are in use (e.g. etags)

Pay attention to the use of authentication and authorization error codes:

  • 401 Unauthorized: Request failed because user is not authenticated
  • 403 Forbidden: Request failed because user does not have authorization to access a specific resource

Return suitable codes to provide additional information in case of errors:

  • 400 Bad Request: The request is malformed (the request body does not parse)
  • 404 Not Found: Requesting for a resource that does not exist
  • 405 Method Not Allowed: Requested HTTP method is not available for a given resource
  • 406 Not Acceptable: Missing or incorrect Accept header was provided as part of the request
  • 410 Gone: Indicates that the resource is no longer available. Useful as a blanket response for old API resource versions
  • 414 URI Too Long: Indicates that the URI requested by the client is longer than the server is willing to interpret
  • 415 Unsupported Media Type: If incorrect Content-type header was provided as part of the request
  • 422 Unprocessable Entity: Used for validation errors – request was understood, but contained invalid parameters
  • 429 Too Many Requests: When a request is rejected due to rate limiting
  • 500 Internal Server Error: Something is wrong with the server
  • 501 Not Implemented: This feature is not ready yet
  • 502 Bad Gateway: Something is wrong with the server
  • 503 Service Unavailable: Unable to connect to the service
  • 504 Gateway Timeout: Something is wrong with the network layer (Load balancer or Edge service cannot connect to the service because of timeout)

Validate request parameters

In some situations you need to validate parameters before you send the create request (POST) to a given resource. It is particularly common in case of frontend, because you want to check whether provided data is acceptable, as only backend service has updated knowledge about how to validate parameters. You can reuse an existing method for creating resources by giving information about dry-run in a query string, e.g.:

curl -X POST https://api.allegro.pl/users?dryRun=true  \
    -d {"login": "userLogin", "password": "userPassword", "email": "user@allegro.pl"}
    -H "Content-type: application/vnd.allegro.public.v1+json"
    -H "Accept: application/vnd.allegro.public.v1+json"

Sample response:

Response status: 204 No content

Validate errors

For error validation, return HTTP 422 Unprocessable Entity response body with a list of errors. Sample error validation response:

{
	"errors": [
	    {
	        "message": "Provided email address does not match pattern",
	        "code": "EmailFormatNotValidException",
	        "details": null,
	        "path": "email",
	        "userMessage": "Pole password musi zawierać małe i duże litery oraz przynajmniej jedną cyfrę."
	    }
    ]
}

Validate selected fields

Use include in query string to specify what parameters you want to validate, e.g. to validate only login and password fields, use include=login&include=password:

curl -X POST https://api.allegro.pl/users?dryRun=true&include=login&include=password  \
    -d {"login": "userLogin", "password": "userPassword", "email": "user@allegro.pl"}
    -H "Content-type: application/vnd.allegro.public.v1+json"
    -H "Accept: application/vnd.allegro.public.v1+json"

Sample response:

Response status 204 No Content

Representation

Property name format

Property names should be meaningful. Use camel case in parameters and properties (e.g. firstName) instead of underscoring (e.g. first_name):

"firstName": "John",
"lastName": "Smith"

Array types should have plural property names. All other property names should be singular.

{
  // Singular
  "user": "123456",
  // An array of siblings, plural
  "offers": [{},{}],
  // "totalItem" doesn't sound right
  "totalCount": 10,
  // But maybe "offersCount" or just "count" is better
  "offersCount": 10,
}

Provide resource (UU)IDs

Assign an id attribute by default to each resource. Use UUIDs unless you have a very good reason not to. Do not use IDs that will not be globally unique across instances of the service or other resources in the service, in particular auto-incrementing IDs.

Render UUIDs as a lowercase string in 8-4-4-4-12 format, e.g.:

"id": "01234567-89ab-cdef-0123-456789abcdef"

Null values

Blank fields are generally included as null instead of being blank strings or omitted. If a property state or value is unknown, consider setting the property to null.

Example of an unknown value or state:

{
  // ...
  "buyerAddresses":[
        {
           "type":1,
           "company":null,
           "name":"Jaś Fasola",
           "street":"Zakręt 56",
           "postCode":"00-999",
           "city":"Lądek Zdrój",
           "province":"Pomorskie"
        }
     ],
  // ...
}

Empty collections

If you want to return empty collection, return [] instead of null. Some client frameworks cannot iterate over null and need extra null checking. Iteration over [] is always possible. e.g.:

{
	"status": "OK",
	"errors": []
}

Use UTC times formatted in ISO8601

Accept and return times in UTC only. Render times as a string ISO8601 format (yyyy-MM-dd'T'HH:mm:ss.SSSZ), e.g.:

{
  // ...
  "createdAt": "2012-01-01T12:00:00.000Z",
  "updatedAt": "2012-01-01T13:00:00.000Z",
  // ...
}

Time without date

Render time value as a string in format (HH:mm:ss.SSS) without time zone information, e.g.:

"from": "08:00:00.000",
"to": "16:00:00.000"

There is no need to add information about a time zone – a client should present value received from an API without any modification. If you need to present time in a specific time zone, use UTC for full date and time.

Country, language and translations

Value representation

If you need to send some property that indicates a country then use the ISO 3166 Alpha-2 codes for that - 2 letters, always upper-cased.

Sample JSON with such a property:

{
    "address" : {
        // ...
        "city" : "Poznań"
        "countryCode": "PL"
    }
}

If you want to refer to a language saved in the property, use the RFC 1766 language tag. It combines the previously mentioned ISO 3166 country codes and ISO 639 language names.

Sample JSON with such a property:

{
    "user" : {
        // ...
        "prefferedLanguage": "en-GB" //  form of English in the UK, but also countries such as Canada
    }
}

Java sample for Locale supporting these formats:

Locale locale = Locale.forLanguageTag("en-GB");
String countryCode = locale.getCountry();
// countryCode == "GB"

The above actually parses according to the RFC 5646 standard which is a backward compatible update to the previously mentioned RFC 1766.

Translations

In the section above we presented how the values of country and language references should be represented in JSON, query params, etc. Translating user messages for the client is a different topic. The HTTP standard defines an appropriate header for that: Accept-Language RFC 2616, 14.4. If the client desires to receive localized user messages in the response, it should set the Accept-Language header in its request.

This is required to handle at least two types of "Accept-Language" :

Accept-Language: en-* (* is any valid type eg. "GB", "US")

and

Accept-Language: pl-PL

By sending the information presented above, you will inform the backing service that all user messages (including error messages) should be translated to English or Polish, respectively. If "Accept-Language" header is omitted or set to an unsupported value then the language should fallback to en-US.

Price and currency

All prices (included in response and request bodies) are requested and returned as a structure with amount and currency fields as proposed by paypal, e.g.

{
    "buyNow": {
        "amount": "11.25",
        "currency": "PLN"
    }
}
  • currency - 3 letter currency code as defined in ISO 4217
  • amount - string representation, at least one digit before a decimal separator without the thousands separator, then dot (.) as a decimal separator, then at most two digits after the decimal separator.

Such a method of presenting amount makes the interpretation easier, provides with precision and protects against rounding errors.

Some amount examples:

  • "0.00"
  • "0.01"
  • "1.00"
  • "1.10"
  • "11.25"
  • "1234567.25"
  • "9999999.99"

If you want to submit price amount and currency as a response to the GET request, you should use:

curl https://api.allegro.pl/contests?totalAmount.amount=11.25&totalAmount.currency=PLN  \
    -H "Accept: application/vnd.allegro.public.v1+json"

Enum values

Enum values are represented as uppercase strings.

As API grows, enum values may be added, removed or changed. Using strings as enum values ensures that downstream clients can gracefully handle changes to enum values.

Java code:

public enum Color {
  WHITE,
  BLACK,
  RED,
  YELLOW,
  BLUE
}

JSON Object:

{
  "color": "WHITE"
}

Nesting foreign resources relations

Nest foreign resources references, even if the only information is id of the object referred to, with a nested object, e.g.:

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "offer-name",
  "seller": {
    "id": "5d8201b0...",
    "name": "user-name"
  },
  // ...
}

Instead of a flat structure e.g.:

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "offer-name",
  "sellerId": "5d8201b0...",
  "sellerName": "user-name"
  // ...
}

This approach makes it possible to inline more information about the related resource without having to change the structure of the response or introduce more top-level response fields, e.g.:

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "offer-name",
  "seller": {
    "id": "5d8201b0...",
    "name": "user-name",
    "email": "user@allegrogroup.com"
  },
  // ...
}

Provide full resources where available

Provide the full resource representation (i.e. the object with all properties) whenever possible in the response.

Accept JSON in request bodies

Accept JSON as Content-Type data in PUT/PATCH/POST request bodies, either instead of or in addition to form-encoded data. This creates symmetry with JSON response bodies, e.g.:

curl -X POST https://api.allegro.pl/users \
    -H "Content-Type: application/vnd.allegro.public.v1+json" \
    -d '{"name": "user-name"}'
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "user-name",
  // ...
}

Keep JSON response minified

Extra whitespace adds needless response size to requests, and many clients (e.g. web browsers) will automatically "improve" JSON output. It is best to keep JSON responses minified e.g.:

{"id": "01234567-89ab-cdef-0123-456789abcdef","name":"offer-name","seller":{"id":"5d8201b0...",
"name":"user-name","email":"user@allegrogroup.com"}
}

Instead of e.g.:

{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "name": "offer-name",
  "seller": {
    "id": "5d8201b0...",
    "name": "user-name",
    "email": "user@allegrogroup.com"
  }
}

Filtering

Simple filters in query string

Use a query parameter for each field that supports filtering (most of them probably do not provide such support; you should document what fields are filterable). For example, when submitting a request for a list of general delivery points to the /general-deliveries endpoint, you may want to limit them to the given name and city. To do so, use a request such as:

curl -X GET https://api.allegro.pl/general-deliveries?name=UP+Poznań+41&address.city=Poznań -H "Accept: application/vnd.allegro.public.v1+json"

Here, name and address.city are fields that support filtering.

Simple rules for query string filters:

  • refer directly to collection entities (name, not points.name)
  • use "." (dot) to indicate a (unique) field name in nested structure (address.city, not just city)

Sample response:

{
  "points": [
    {
      "id": "de305d54-75b4-431b-adb2-eb6b9e546014",
      "name": "UP Poznań 41",
      "address": {
        "street": "Ulica Starołęcka 42",
        "code": "61-360",
        "city": "Poznań"
      }
    }
  ]
}

To support ranges in filtering use virtual fields with suffixes :

  • gt - greater than
  • lt - less than
  • gte - greater or equal
  • lte - less or equal

Example request:

curl -X GET https://api.allegro.pl/general-deliveries?rate.gt=200 -H "Accept: application/vnd.allegro.public.v1+json"

Filter by multiple values in one filed

If you want to filter many values in one field, you should repeat the field name many times in URL adding different values, e.g. to filter shipping payments in PRE and POST values use:

curl -X GET https://api.allegro.pl/general-deliveries?shipments.payments=PRE&shipments.payments=POST -H "Accept: application/vnd.allegro.public.v1+json"

This type of convention, i.e. shipments.payments=PRE&shipments.payments=POST is supported by many frameworks out of the box.

Advanced filtering (similar to search) concerning many parameters and nested structures

For advanced filtering:

  • create search id from your data – sending POST body to "new resource" (e.g. /product-searches) will return search id (in Location header) for a query
  • get collection for search id – use search id returned after the operation mentioned above to get expected collection of entities by calling base resource (e.g. /products?search.id=)
  • get search data for search id – use search id returned after the operation mentioned above to get search data by calling "new resource" (e.g. /product-searches)

Rules:

  • "new resource" should be presented in /*-searches convention where the base resource name is singular, e.g. in case of /products it should be named /product-searches
  • entity in a request POST body in /*-searches (e.g. /product-searches)
  • handle query string parameter search.id in base resource (e.g. /products) to handle filters
  • entities created by a "new resource" should be versioned as the base resource – it should be possible to get a suitable version of /product-searches resource using any search.id for each new version of e.g. /products resource
  • the new resource version should not break backward compatibility (if possible)

Sample entities in /products collection

{
    "products": [
        {
            "id": "7d3e4d5a-817c-45f8-930b-e319dbcedc5c",
            "name": "Doładowanie Heyah 5 zł",
            "parameterGroups": [
                {
                    "id": "4be770c3-6805-4967-8f13-0457dc8ed446",
                    "name": "Bazowe informacje",
                    "parameters": [
                        {
                            "id": "ce87a49d-55c3-476c-b7f8-349dfaf89510",
                            "name": "Rodzaj usługi",
                            "values": [
                                "Doładowanie"
                            ]
                        },
                        {
                            "id": "63baaaf2-534a-4c20-b1ed-e44952d7e556",
                            "name": "Nazwa",
                            "values": [
                                "Doładowanie Heyah 5 zł"
                            ]
                        },
                        {
                            "id": "ab98c5ec-f9c6-440f-9af0-a19caaba7eca",
                            "name": "Operator",
                            "values": [
                                "Heyah"
                            ]
                        },
                        {
                            "id": "7fdd3aa4-3cab-4d4f-93c4-9c2ff05bec86",
                            "name": "Wartość doładowania",
                            "values": [
                                "5 zł"
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

Let’s assume you are looking for products with:

  • parameterGroups.id with value 4be770c3-6805-4967-8f13-0457dc8ed446
  • dynamic parameters with many values, e.g. a dynamic parameter of id ce87a49d-55c3-476c-b7f8-349dfaf89510 with value Doładowanie and parameters of id ab98c5ec-f9c6-440f-9af0-a19caaba7eca with value Heyah

you can submit a request for /product-searches:

curl -X POST https://api.allegro.pl/product-searches -H "Accept: application/vnd.allegro.public.v1+json" -d
{
    "limit": 100,
    "offset": 150,
    "parameterGroups": [
        {
            "id": "4be770c3-6805-4967-8f13-0457dc8ed446",
            "parameters": [
                {
                    "id": "ce87a49d-55c3-476c-b7f8-349dfaf89510",
                    "values": [
                        "Doładowanie"
                    ]
                },
                {
                    "id": "ab98c5ec-f9c6-440f-9af0-a19caaba7eca",
                    "values": [
                        "Heyah"
                    ]
                }
            ]
        }
    ]
}

In the sample above server should write down the version of search data used to create it from the Accept header – application/vnd.allegro.public.v1+json. It will improve the process of mapping from one search data version to another in the future.

In return, you get an id for the created "search" (in Location header, status HTTP 201 Created), which holds your search results. To view them, provide:

curl -X GET https://api.allegro.pl/product-searches/4be770c3-4967-6805-8f13-0457dc8ed446 -H "Accept: application/vnd.allegro.public.v1+json"

Important note: There is no way to override filters for a given id - you must create a new search resource.

Use search.id parameter to get filtered collection of entities with a sort parameter such as:

curl -X GET https://api.allegro.pl/products?search.id=4be770c3-4967-6805-8f13-0457dc8ed446&sort=-parameterGroups.name -H "Accept: application/vnd.allegro.public.v1+json"

Sorting

Sorting: A generic parameter sort can be used to describe sorting rules. Adjust complex sorting requirements by letting the sort parameter take in a list of comma separated fields, each with a possible unary negative to imply descending sort order. Let's look at some examples:

  • GET /offers?sort=-buyNow – Retrieves a list of offers put in a descending order by buyNow price
  • GET /offers?sort=-buyNow,createdAt – Retrieves a list of offers put in a descending order by buyNow price. Within a specific buyNow price, older offers are presented as first.

Wrap collection in object

Always return root element as an object. This way you can add extra metadata fields to the response without compatibility breakdown.

For example, in the case of the offers collection, you can add a metadata field such as count containing a number of matching offers:

{
    "offers": [
        {
            "id": "01234567-89ab-cdef-0123-456789abcdef",
            "name": "PINK FLOYD: THE ENDLESS RIVER"
        },
        {
            "id": "11234567-89ab-cdef-0123-456789abcdef",
            "name": "Ufomammut - Eve"
        }
    ],
    "count": 4674
}

When a collection is in the root, you cannot add extra field there.

[
    {
        "id": "01234567-89ab-cdef-0123-456789abcdef",
        "name": "PINK FLOYD: THE ENDLESS RIVER"
    },
    {
        "id": "11234567-89ab-cdef-0123-456789abcdef",
        "name": "Ufomammut - Eve"
    }
]

Metadata should only contain direct properties of the response set, not properties of the members of the response set.

Consistent paging scheme

Use offset instead of page and limit parameters. It is much more flexible for the client, e.g.

GET /offers?offset=0&limit=100
GET /offers?offset=100&limit=500

Keep response gziped

All responses should be gziped based on the client Accept-Encoding: gzip header and return information about used encode method in the Content-Encoding: gzip header.

Error

Provide Trace-Ids for Introspection

Include the Trace-Id header in each API response that contains an UUID value. By logging these values on the client, server and any backing services, it provides a mechanism to trace, diagnose and debug requests.

Generate structured errors

A client sets the requested Accept-Language header (RFC code) to get a localized userMessage (see Translations for more info):

Accept-Language: ru-UA

In case of an error response, the client gets:

HTTP/1.1 422 Unprocessable Entity
{
	"errors": [
	    {
	        "message": "Delivery point data not passed",
	        "code": "MissingDeliveryPointException",
	        "details": "Exception was thrown from https://api.allegro.pl/points/1 in line 2",
	        "path": "Endpoint.getDeliveries.arg1",
	        "userMessage": "Nie wybrano punktu dla odbioru osobistego."
	    }
    ]
}

Generate consistent, structured response bodies concerning errors.

  • message - internal error message for a developer,
  • code - error code in a string representation; it can be an exception name e.g. MissingDeliveryPointException,
  • details - more specific message for an internal developer; it will be null in case of a production environment,
  • path - information about failed parameters validation; can be null,
  • userMessage - localized message (based on Accept-Language header) to be presented to an application user (much more general than a message); this field is mandatory.

A developer can use Trace-Id header from a response to determine the exact flow for a given error in micro-services.

Command pattern

Problem

Mapping CRUD operations to semantics of HTTP POST, PUT, DELETE is easy. However that is not the case for more complex operations that do more than simply send the new state of a single resource. An example of such operations is the renewal of offers on Allegro - the operation requires some input data and modifies lots of fields in the given offer causing business changes in many integrated services (settlement, fraud monitoring, etc.).

How to model this type of a non-trivial operation in REST? It can be a change caused by updating the offers status as presented below:

curl -X PUT https://api.allegro.pl/offers/6546456 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
{
    "status" : "RENEWED",
    // all other offer fields ...
}

or this way:

curl -X PATCH https://api.allegro.pl/offers/6546456 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
[
    {
        "op" : "replace",
        "path" : "/status",
        "value" : "RENEWED"
    }
]

However, it looks as programming database systems from the 90's where an update of one field would unleash dozens of triggers with hidden business logic, to the surprise (and dismay) of the developer.

Beside this risky programming style, you have to ask yourself the following questions:

  • what if the given operation requires input data that is not present in the resources model?
  • what if the operation is so complex that you must execute it asynchronously?
  • what should be returned in response to PUT or PATCH then? how to trace the status of that asynchronous operation?

Solution

All the above problems can be solved by using the command pattern that gives you:

  • a verbose declaration of more complex operations in the system,
  • a standard mechanism to trace the status of asynchronous operations,
  • an idempotent way to execute this operation.

To implement this pattern add a sub-resource with commands to your business resource, for example: /offers/{offerId}/{command-type}-commands. In this example if you want to execute a complex operation on an offer add the operation input data to the appropriate commands collection. But do not use POST to do it as POST is not idempotent in REST – use the PUT method and an UUID generated by the client.

Adding the command

curl -X PUT https://api.allegro.pl/offers/6546456/renew-commands/23453425-34253245-3453454-345345 -H "Content-Type: application/vnd.allegro.public.v1+json" -d
{
    "fromDate" : "2015-08-30T17:00:00.000Z",
    "duration" : "P7D",
    // other input data for this command
}

Although many developers are used to apply PUT method only for updates, you must be aware that the semantics of this method is much wider according to the HTTP RFC. See RFC 7231 for more details.

Sample response:

201 Created
{
    "status" : "RUNNING",
    "fromDate" : "2015-08-30T17:00:00.000Z",
    "duration" : "P7D",
    // other output data given to this command
}

After adding this command, the service will execute it asynchronously. It should return the HTTP 201 Created status code even if you send this request many times. however the business logic will be executed only once.

Checking execution status (optionally)

curl -X GET https://api.allegro.pl/offers/6546456/renew-commands/23453425-34253245-3453454-345345 -H "Accept: application/vnd.allegro.public.v1+json"

Response:

200 Ok
{
    "status" : "SUCCESSFUL",
    "fromDate" : "2015-08-30T17:00:00.000Z",
    "duration" : "P7D",
    // other output data given to this command
}

or if the execution of the command failed:

200 Ok
{
    "status" : "FAILED",
    "fromDate" : "2015-08-30T17:00:00.000Z",
    "duration" : "P7D",
    // other output data given to this command
    "errors" : [
        // errors description
    ]
}

It is also recommended to provide information about command status changes in an event bus. Developers will decide which mechanism they prefer.

Antypatterns replaced by this pattern

  • POST-based command pattern that submits commands directly to a resource such as e.g. /offers/546534534
    • according to the RFC, the POST method is for not idempotent operations
    • no guarantee on the API level that the business logic will be executed only once
  • sending command UUID's in a header
    • this pattern is incompatible with REST and the HTTP RFC which states that non-standard headers are deprecated
    • does not solve all the problems that the PUT and UUID in URI pattern solves

Documentation

Each resource method (GET, POST, PUT, DELETE, PATCH) has to provide brief documentation for itself. There should be a description of what given resource is responsible for and how a client interprets it. This documentation should contain information on expected input parameters (GET) or body (POST, PUT, PATCH), required and optional parameters, the response a client should expect and possible error codes. Documentation should be compatible with swagger spec 2.0 and should be accessible under a well-known endpoint (e.g. service-host/swagger.json).

Glossary

The main goal of the glossary is to unify terms used by public resources in order to give a clear understanding of the Allegro REST API and to make the integration with Allegro smooth. Below terms should be used in resource’s names, models, objects, query parameters, etc. The glossary is divided into several sections, each of them represents a specific area of Allegro domain or specific technological aspect (e.g. how to represent a user).

The main goal of the glossary is to unify terms of all our public resources in order to give a clear understanding of the Allegro REST API and to help our clients seamlessly integrate with the Allegro. Below terms should be used in our resources' names, models, JSON objects, query parameters etc. The glossary was divided into several sections, each of them represents some area of Allegro domain or specific technological aspect (e.g. how to represent a user). You are invited to contribute the glossary, especially when you are responsible for some area in Allegro, just create a pull-request.

General

  • id - entity's identifier (i.e. offer, category)
  • name - used to store name of an entity (i.e. offer, category)
  • dryRun - should a function run without any modification of data?
  • active - is the entity active?
  • inactive - is the entity inactive?
  • version - version of an entity or a system
  • count - number of matched entities (e.g. in case of pagination), used mainly for pagination where it should tell how many entities matched the clients criteria, NOT how many entities were returned on the given page
  • language - according to language tag (https://tools.ietf.org/html/bcp47, https://en.wikipedia.org/wiki/IETF_language_tag)

Address

  • address
  • street
  • city
  • province
  • postCode
  • countryCode

Image

  • image - keep in mind that we do not use picture
  • url - image url
  • title - image title
{
    "image": {
        "url": "",
        "title": ""
    }
}

Description

  • description
  • summary - brief text summary
  • text - full text description
{
    "description": {
        "summary": "",
        "text": ""
    }
}

Category

  • parent - category parent
  • leaf - is the category a leaf?
  • tree - to which category tree it belongs to?
{
    "category": {
        "id": "...",
        "name": "...",
        "leaf": true,
        "tree": {
            "name": "sample-tree"
        }
    }
}

User

  • user - Allegro user entity
  • password - user's password
  • firstName
  • lastName
  • companyName
  • sex
  • phoneNumber
  • email - user's email
  • birthDate - user's birth date
  • confirmation
  • registration

Coordinates

  • coordinates - map coordinates
  • lat - latitude
  • lon - longitude
{
    "coordinates": {
        "lat": 52.33374,
        "lon": 16.808437
    }
}

Pagination

  • offset - page index (do not use page, pageIndex, etc.)
  • limit - page size (do not use pageSize, size, length, etc.)

Try to avoid

  • metadata - do not use it. Almost all information from this object could be moved to parent entity
  • picture - use image instead
  • page, pageIndex, pageNo, pageNumber - do not use them; in case of pagination use offset instead

HATEOAS

TL;DR We do not use HATEOAS and do not support it

RESTful design principles specify HATEOAS which roughly states that interaction with an endpoint should be defined within metadata that comes with the output representation and not based on out-of-band information. HATEOAS also says that all the actions available for a given resource, all its state transitions, plus all the relations between resources must be a complete URL that uniquely defines the resource.

Although the web generally works on HATEOAS type principles (where we go to a website's front page and follow links based on what we see on the page), we are not ready for HATEOAS on APIs just yet. When browsing a website, decisions on what links will be clicked are made at run time. However, with an API, decisions as to what requests will be sent are made when the API integration code is written, not at run time. Moreover the entire logic of mobile applications which are the main clients for the API should be rewritten either by including URIs in domain model objects to be passed to controllers or by changing the contracts of the controllers to include the URIs as well as objects.