API Guidelines

Request for comment

Table of contents

Table of contents

1. Introduction

Welcome to Enturs API guidelines, designed to ensure a consistent and robust approach to developing RESTful APIs. REST (Representational State Transfer) is an architectural style that uses standard HTTP methods to create scalable, lightweight, and maintainable services.

1.1 Purpose of the Entur API guidelines

  • To establish common guidelines and best practices for API design
  • To simplify integration between different systems and applications
  • To contribute to a secure, efficient and user-friendly implementation of APIs

1.2 Intended audience

This guide is for developers, architects, and technical designers who work on the design, implementation, and maintenance of APIs. By following these guidelines, we ensure APIs are easy to understand, consistent in structure, and simple to integrate with other systems.

1.3 Contributing

For details on how to contribute to these guidelines, see the contributing guide.

1.4 Requirement Levels

Throughout this document, these requirement levels are used:

  • MUST: This is an absolute requirement
  • SHOULD: There may be valid reasons to ignore this requirement, but implications must be understood and carefully weighed
  • MAY: This is optional

1.5 Linter Coverage

Throughout this document, rules are marked with the following indicators:

  • :white_check_mark: Automatically enforced by linter
  • :ballot_box_with_check: Partially checked by linter
  • :eyes: Requires manual review

2. Core Principles

2.1 General Design Principles

  • :eyes: Consistency - Make sure the API is easy to understand and predictable
  • :white_check_mark: HTTP Methods - API operations MUST use standard HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • :white_check_mark: Data Format - API endpoints SHOULD support the JSON data format, unless there is a good reason not to
  • :white_check_mark: APIs MUST be documented using OpenAPI 3.x
  • :white_check_mark: Documentation - All functionality SHOULD be documented with examples and descriptions
  • :white_check_mark: Encryption: All communication MUST be over HTTPS
  • :white_check_mark: You SHOULD not use localhost (or 127.0.0.1) host names in info.servers.
  • :white_check_mark: info.title MUST be non-empty and MUST not contain the word ‘api’.
  • :eyes: Don’t use info.title to declare your APIs lifecycle status (draft, beta, deprecated). Instead follow the lifecycle guidelines.

2.2 Development Approach

  • :eyes: We encourage to follow a Contract-First workflow:
    • Create the API specification before implementing the API
    • The specification is the primary reference for both development and documentation
    • Update the specification throughout development to reflect changes
  • :eyes: Lint your API spec
  • :eyes: Differentiate API specifications based on the target audience. The target audience of an API specification is declared in info.x-entur-metadata.audience. For details, see: 2.4 Entur Metadata. There are different requirements based on the audience of your API.
    • open: Available to anyone, without requiring a contractual relationship with Entur
      • :eyes: MUST follow these guidelines
      • :eyes: MUST be published to the developer portal
    • partner: Available to partners of Entur
      • :eyes: MUST follow these guidelines
      • :eyes: MUST be published to the developer portal
    • internal: Available to other teams within Entur
      • :eyes: SHOULD follow these guidelines
      • :eyes: MUST be published to the developer portal
    • private: Only available from within the same component/application. Examples: A backend API made specifically for a frontend developed by the same team, or administration and management endpoints used for housekeeping within the team.
      • :eyes: MAY follow these guidelines
      • :eyes: MAY be published to the developer portal

2.3 Authentication and Authorization

2.3.1 About security in OpenAPI specs

Use securitySchemes inside components to define security schemes. Then, refer to schemes using security either at the root level of the spec,to apply security to all operations, or under individual operations which are secured, which replaces the root config.

See:

  • https://spec.openapis.org/oas/v3.1.2.html#security-scheme-object
  • https://spec.openapis.org/oas/v3.1.2.html#security-requirement-object

    2.3.2 Partner- and Internal endpoints

  • :eyes: These endpoints are secured using JWT tokens. This MUST be documented using a jwt scheme.

Example:

{
  "security": [{ "jwt": [] }],
  "components": {
    "securitySchemes": {
      "jwt": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
    }
  }
}

More information on Authentication

2.3.3 Documenting permissions for Partner endpoints

  • :ballot_box_with_check: Endpoints that require permissions MUST be documented with the x-entur-permissions extension.

Example:

{
  ...,
  "paths": {
    "/items": {
      "get": {
        "operationId": "getItems",
        ...,
        "x-entur-permissions": {
          "value": "items:les"
        }
      },
      "post": {
        "operationId": "createItem",
        ...,
        "x-entur-permissions": {
          "value": "items:opprett"
        }
      }
    }
  }
}

where items:les means that you need the access les on the operation items.

You can also specify that an endpoint requires multiple permissions, for example:

{
  "x-entur-permissions": {
    "value": {
      "all": [
        "organisations:les",
        {
          "any": [
            "items:opprett",
            "items-global:opprett"
          ]
        }
      ]
    }
  }
}

This means that the endpoint requires organisations:les, as well as either items:opprett or items-global:opprett.

If you need to explain the required permissions in more detail, you can declare a description field at the root.

{
  "x-entur-permissions": {
    "description": "To call this endpoint you need access to read organisations, as well as creating items for your organisation.",
    "value": {
      "all": [
        "organisations:les",
        {
          "any": [
            "items:opprett",
            "items-global:opprett"
          ]
        }
      ]
    }
  }
}

2.4 Entur Metadata

All OpenAPI specifications published to Enturs developer portal must declare a block x-entur-metadata in the info section of the specification.

Field name Type Description
id string REQUIRED. Unique id for this specification. Read more.
audience string REQUIRED. Who this specification is targeted to. Must be one of "open", "partner", "internal", "private"
owner string REQUIRED. The Entur team responsible for this specification. Read more.
parentId string Id of the parent specification, used when merging. Read more.
devExtensions [string] OpenAPI extensions that are used for development, that should not be visible to consumers. Read more.

Example:

{
  "info": {
    "x-entur-metadata": {
      "id": "items",
      "owner": "team-api",
      "audience": "partner"
    }
  }
}

2.4.1 Identifying a specification

  • :white_check_mark: OpenAPI specifications MUST use the id property in the x-entur-metadata extension in the info section.
  • :white_check_mark: It MUST be in lower kebab-case and contain only dashes, digits and letters (a-z).
  • :eyes: It SHOULD NOT have a -id suffix (or prefix).

Choosing an id The id is used to uniquely identify a specification. Each id results in an entry in the Developer Portal API catalogue and an entry in the linting results. It is also used to create the URL where the specification is available for download from the Developer Portal. Because of this, the id should not change over time, and it should be suitable for external use. The current api title (in kebab-case) could be a good id, but you should not update the id if the title changes in the future.

See also 3.2.2 API spec versioning.

2.4.2 Specification owner

  • :white_check_mark: OpenAPI specifications SHOULD use the owner property in the x-entur-metadata extension in the info section.

The owner field declares which team is responsible for maintaining a specification. It should follow the same format as the owner field in the platform orchestrator.

2.4.3 Merging specifications

Sometimes it is useful to develop some API endpoints separately, but still document them externally as a single specification. To achieve this, you can use the x-entur-metadata field parentId. This field may contain a reference the x-entur-metadata.id field in another published specification.

Nesting is not supported. parentId must refer to a specification that does not itself declare a parentId.

If there are problems with the merging, for example conflicts between the schemas, none of the specifications will be exposed.

Limitations

There are some limitations when merging specifications:

  • servers.url in all specs must be a subpath of the declared servers.url in the “parent specification”. The merged specification will use the servers definition from the parent specification, and the subpath of the other specifications will be added to the operation paths. If servers are not compatible, none of the specifications will be published.
  • Root level security must be identical in all specs to be merged.
  • If there are multiple non-equal schemas with the same name, they will be namespaced with the x-entur-metadata.id field. So if multiple specs declare a schema Item that differ from each other, there will be one schema Item_alpha and one schema Item_beta.
Example

For example, say you have three microservices, alpha, beta and gamma:

Microservice alpha publishes the specification:

{
  "info": {
    "x-entur-metadata": {
      "id": "alpha"
    }
  },
  ...
}

Microservice beta publishes the specification:

{
  "info": {
    "x-entur-metadata": {
      "id": "beta",
      "parentId": "alpha"
    }
  },
  ...
}

Microservice gamma publishes the specification:

{
  "info": {
    "x-entur-metadata": {
      "id": "gamma",
      "parentId": "alpha"
    }
  },
  ...
}

Here, only 1 specification will be shown on the Developer Portal, which is a combination of alpha, beta and gamma. Since alpha is the parent specification, the combined specification will use alpha as the base, so fields like info.title will be picked from there.

2.4.4 Development-only OpenAPI extensions

Sometimes there may be a need for adding OpenAPI extensions to a specification that are only meant for the development process, for example for generating server interfaces with OpenAPIGenerator. These extensions are only relevant for the team creating the API, not for consumers. To hide extensions like these from the OpenAPI specification that is served from the Developer Portal, use the field x-entur-metadata.devExtensions.

Example

The extension x-kotlin-implements will be stripped before the specification is published to the Developer Portal.

{
  "info": {
    "x-entur-metadata": {
      ...,
      "devExtensions": ["x-kotlin-implements"]
    }
  },
  "components": {
    "schemas": {
      "Item": {
        "type": "object",
        "x-kotlin-implements": ["com.example.Item"],
        ...
      }
    }
  }
}

2.5 Lifecycle

All APIs, and endpoints within an API, follow a lifecycle with different stages that have different properties. The following sections describe a standardized way to declare an API’s lifecycle status in its specification. Following this standard has several benefits:

  • It is easier for consumers to understand what they can expect from an API in terms of stability.
  • It is easier to enforce Enturs common policy on backwards compatibility and deprecation.

Lifecycle status may be declared both for the entire API specification, or for individual operations (endpoints). At the API level, use the following extension fields in the info block:

Field name Type Description
x-stability-level string The current lifecycle status of the API. Must be one of: draft, beta, stable. Default value is stable.
x-deprecated boolean true if the API is deprecated. Default value is false.
x-sunset string The date at which the API will be discontinued, in the format YYYY-MM-DD.

For individual operations, use the following extension fields in an Operation object. All of the fields inherit default values from the API level.

Field name Type Description
deprecated boolean Already exists in the OAS. true if the endpoint is deprecated. Default value is info.x-deprecated.
x-stability-level string The current lifecycle status of the endpoint. Must be one of: draft, beta, stable. Default value is info.x-stability-level.
x-sunset string The date at which the endpoint will be removed, in the format YYYY-MM-DD. Default value is info.x-sunset.

Below is a description of what values these fields should have during an API’s or endpoint’s lifecycle.

2.5.1 Design and development

The shape of the API is being figured out. Changes happen often. The API may not even be implemented yet.

  • :ballot_box_with_check: The field x-stability-level MUST have the value draft
  • :ballot_box_with_check: The field deprecated/x-deprecated MUST have the value false (or be empty)
  • :ballot_box_with_check: The field x-sunset MUST be empty
  • :ballot_box_with_check: You MAY make breaking changes

2.5.2 Testing and feedback

Changes happen, and more design and development may be necessary.

  • :ballot_box_with_check: The field x-stability-level MUST have the value beta
  • :ballot_box_with_check: The field deprecated/x-deprecated MUST have the value false (or be empty)
  • :ballot_box_with_check: The field x-sunset MUST be empty
  • :ballot_box_with_check: You MAY make breaking changes

2.5.3 Production use

The API is relied upon by consumers. Changes may happen, as long as they are not breaking.

  • :ballot_box_with_check: The field x-stability-level MUST have the value stable (or be empty)
  • :ballot_box_with_check: The field deprecated/x-deprecated MUST have the value false (or be empty)
  • :ballot_box_with_check: The field x-sunset MUST be empty
  • :ballot_box_with_check: You MUST NOT make breaking changes

2.5.4 Deprecation

The API is marked as deprecated and consumers are encouraged to move to a new version. Only critical changes will happen.

  • :ballot_box_with_check: The field x-stability-level MUST have the value stable (or be empty)
  • :ballot_box_with_check: The field deprecated/x-deprecated MUST have the value true
  • :ballot_box_with_check: The field x-sunset MUST be filled with a date that follows Enturs rules for deprecation
  • :ballot_box_with_check: You MUST NOT make breaking changes

2.5.5 Sunsetting

The API is removed.

  • :ballot_box_with_check: When the date in x-sunset has passed, the endpoint or API SHOULD be removed. If the API is not ready to be removed yet, the value should be updated.

2.5.6 Notes

  • :ballot_box_with_check: Individual endpoints MAY override x-stability-level to an earlier stage in the lifecycle than the API, but NOT later.
    • For example, if stable is declared at the API level, endpoints may be draft, beta or stable, but if beta is declared at the API level, endpoints may only be draft or beta.
  • :ballot_box_with_check: Individual endpoints MUST NOT set deprecated to false, if the API has set x-deprecated to true.
  • :ballot_box_with_check: Individual endpoints MAY override x-sunset to an earlier date than the API, but NOT later.
  • :ballot_box_with_check: You MAY use the property deprecated on other components in the specification, such as on parameters, headers, schemas or schema properties. However, this should only be used to indicate that the component will be removed in the next version of the API. Removal of a deprecated component is still considered to be a breaking change.

3. Naming & Structure Conventions

3.1 Resource Naming

  • :eyes: You SHOULD use plural Nouns - For example: /customers, /benefits and /offers
    • Exception: Singleton resources that represent a unique entity MAY use singular form, such as /user/profile, /me
  • :eyes: Hierarchical Structure - For related resources you SHOULD use hierarchical URL structure. For example: /orders/{orderId}/fees
  • :eyes: No Actions in URL - Actions (such as create, delete) SHOULD be handled via the HTTP method, not in the URL itself
  • :white_check_mark: URL in kebab-case - URL for APIs MUST be in kebab-case. For example /realtime-deviations/v1/subscription
  • :white_check_mark: Field Names in camelCase - For request and response body field names and query parameters, camelCase MUST be used
  • :eyes: Consistent Terminology - Similar concepts across different APIs SHOULD use consistent naming. For example, don’t mix /users and /people or /proposals and /offers when referring to the same concept across different APIs.
  • :white_check_mark: Server URL MUST be in lowercase
  • :white_check_mark: Paths SHOULD NOT contain “api” in the name

3.2 Versioning

3.2.1 API Versioning

  • :eyes: URL Based Versioning - You MUST include the version number in the URL
  • :eyes: Best Practices on Version Info - You SHOULD place the version number immediately after the domain, and before the resource path itself
    • Example: https://api.entur.io/sales/v1/orders?distributionChannelId=1
  • :eyes: You SHOULD NOT include the stability level of the api in the URL. Instead, follow the lifecycle guidelines
    • Example: :x: https://api.entur.io/sales/v1beta/orders?distributionChannelId=1

3.2.2 API Spec Versioning

API spec versioning is done automatically when publishing the spec. If the published specification has an Entur id that has been published before from the same repository, it will be considered a new version of the same spec. The version is set based on the CalVer versioning convention, and uses the format YYYY.MM.MICRO. When the spec is published, the current year and month is used, and MICRO is set to “00”. If a version already exists for the given year and month, “01” is used (and so on). If the published spec is equal to the current spec, no new version is created. Because versioning is done automatically, the value in info.version is ignored, but must be set in order for linting to pass, therefore a placeholder value like 1.0.0 may be used.

3.3 Tags Naming

:eyes: Tags should be used as a logical grouping that reflects functional domains, use cases or data types, not internal architecture.

  • Bad example: internal, partner, production, route-service-v2, misc, other
  • Good example, when using tags for functional domains: Journey Planning, Realtime Departures
  • Good example, when using tags for use-cases : Search, Booking
  • Good example, when using tags for data types : Routes, Departures

3.4 Enumerations

:eyes: Prefer using open-ended values with examples, instead of enum

The enum (https://json-schema.org/understanding-json-schema/reference/enum) keyword in JSON schema (and therefore inherited to OpenAPI), indicates that a schema property will only have a defined, limited set of values. This is problematic for evolving a schema over time. Adding a new allowed value is considered a breaking change, as the previous contract is no longer valid. Because of this, you SHOULD NOT use the enum keyword, unless you are sure that there will never be a need for new values. Instead, declare your enumeration with an open-ended type, such as string, and use examples to indicate currently known values. For example:

transportMode:
  type: string
  examples:
    - TRAIN
    - BUS

This means that the consumer knows that the field transportMode can have either of those values, but that it may also return other values, so they are encouraged to implement a fallback. On the other side, the producer may add new entries to examples and start to return those values, without it being a breaking change.

4. Communication Standards

4.1 HTTP Status Codes

  • :white_check_mark: Request body is only allowed for PUT, POST and PATCH
Code Description GET POST PUT PATCH DELETE
200 OK :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
201 Created   :white_check_mark: :white_check_mark:    
202 Accepted   :white_check_mark:      
204 No Content   :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
302 Found :white_check_mark:        
303 See Other   :white_check_mark:      
400 Bad Request :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
401 Unauthorized :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
403 Forbidden :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
404 Not Found :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
409 Conflict   :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
410 Gone :white_check_mark:   :white_check_mark: :white_check_mark: :white_check_mark:
412 Precondition Failed     :white_check_mark: :white_check_mark: :white_check_mark:
500 Internal Server Error :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
503 Service Unavailable :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:

4.2 Error Handling

When an error occurs, the IETF standard for Problem Details for HTTP APIs (RFC 9457) SHOULD be followed, with few additional guidelines:

  • :white_check_mark: Error responses MUST either use media type application/problem+json, OR application/problem+xml (if Accept header is application/xml)
  • :white_check_mark: The fields title and status MUST be included
  • :eyes: The detail field SHOULD be included when it provides additional useful information
  • :eyes: The type field MAY be included. If included, it MUST be an absolute URI
  • :eyes: The instance MAY be included. If included, it MUST be an absolute URI

Example:

HTTP/1.1 403 Forbidden Content-Type: application/problem+json Content-Language: en

{
  "type": "https://example.com/request-endpoint",
  "title": "Access forbidden",
  "status": 403,
  "detail": "You do not have permission to access this resource.",
  "instance": "https://example.com/something/something",
  "balance": 30,                        //Custom field
  "recommended-action": "Something."    //Custom field
}

5. Data Formatting Standards

5.1 Language & Spelling

  • :eyes: You MAY allow clients to specify a preferred language via the Accept-Language header or a query parameter.
  • :eyes: You MAY return the Content-Language header to inform clients of language used in response.
  • :ballot_box_with_check: Accept-Language and Content-Language values MUST be valid IETF BCP 47 language tags. BCP 47 uses ISO 639-1 codes when available, and ISO 639-3 codes if no two-letter code exist. E.g. “en”, not “eng”. Also, as specified by BCP 47, clients may send multiple tags with quality values (e.g. nb,en;q=0.9).
  • :ballot_box_with_check: Macrolanguage tags (e.g., “no”) MUST NOT be used - instead use the specific language variant (e.g., “nb” or “nn”).

  • :eyes: British English spelling as defined in the Oxford English Dictionary SHOULD be used for consistency

Example:

GET /api/v1/info Accept-Language: nb

GET /api/v1/info?lang=nb

5.2 Date & Time

  • :eyes: You SHOULD use the ISO 8601 standard for all date and timestamps

Example:

“lastUpdated”: “2025-02-13T15:30:00Z”

5.3 Currency Representation

  • :eyes: Prices should be specified as floating-point numbers, represented as strings
  • :eyes: Request: Max 18 digits total; max 5 decimals (follows ISO 20022)
  • :eyes: Response: Always serialized with the standard number of decimals. For NOK, this means 2, since øre is the smallest unit.

Example:

{
    "amount": "99.00"
    "currency": "NOK"
}

5.4 Character Encoding

  • :eyes: You MUST encode all text in UTF-8
  • :eyes: Set the Content-Type header to for example application/json; charset=utf-8
    • Tip: test the api with international characters (e.g. æ, ø, å)

5.5 HTTP Headers

  • :ballot_box_with_check: HTTP headers MUST use Hyphenated-Pascal-Case format (e.g., Content-Type, Accept-Language)
  • :eyes: Custom HTTP headers SHOULD use the prefix Entur-, with some exceptions.

5.5.1 Known Custom HTTP Headers

ET-Client-Name
  • :white_check_mark: All endpoints SHOULD allow consumers to identify themselves using the request header ET-Client-Name. The header is added automatically to all endpoints in a specification before publication to the developer portal.
Entur-POS

Used to declare which “point-of-sale” a request is coming from.

Entur-Distribution-Channel

Used to declare which distribution channel (sales channel) a request is coming from.

X-Correlation-Id

A “de-facto” standard for correlating a request throughout a microservice architecture. At Entur, this header is handled by our logging library cloud-logging. It is automatically added to all endpoints in a specification before publication to the developer portal.

6. Advanced Design Patterns

6.1 Pagination and Sorting

:eyes: If you implement pagination, you MUST use one of these approaches:

  • Offset pagination with query parameters offset and limit:
    GET /api/v1/bus-stops?city=Oslo&offset=10&limit=20
    
  • Cursor / Keyset Pagination with query parameters cursor and pageSize:
    GET /api/v1/bus-stops?city=Oslo&cursor=eyJpZCI6MTAwfQ&pageSize=20
    

See pagination for more details.

All returned collections MUST be sorted, which it means that the order is deterministic: the same request MUST return items in the same order.

:eyes: If you implement client-controlled sorting, you MUST use query parameter sort:

GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc

See sorting for more details.

6.2 Partial Responses

  • :eyes: You MAY let clients choose which fields to include to reduce data transfer

Example:

GET /api/v1/bus-stops?fields=id,name,location

6.3 Batch Operations

  • :eyes: When multiple operations need to be handled in a single call, the API SHOULD support batch operations

Example:

POST /api/v1/batch
{
 "operations": [
   { "method": "GET", "path": "/v1/resource/1" },
   { "method": "DELETE", "path": "/v1/resource/2" }
 ]
}

6.4 Caching & Resource Expiration

  • :eyes: You MAY use HTTP headers such as Cache-Control, ETag, and Last-Modified

Example:

HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "abc123"
Last-Modified: Fri, 13 Feb 2025 15:30:00 UTC

The client can then cache the result for 3600 seconds, and after that it can issue a conditional GET using the ETag and Last-Modified headers

Example:

GET /your-resource HTTP/1.1  
If-None-Match: "abc123"  
If-Modified-Since: Fri, 13 Feb 2025 15:30:00 UTC

If both If-None-Match and If-Modified-Since are sent, the server MUST ignore If-Modified-Since (RFC7232 §3.3). If the resource has not changed, the server can respond with 304 Not Modified.

Example:

HTTP/1.1 304 Not Modified

6.5 Import & Export Formats

  • :eyes: You SHOULD support JSON, unless you have a good reason not to.
  • :white_check_mark: Use the HTTP Accept header to specify desired response format

Example:

GET /api/v1/data
Accept: application/json

6.6 Validation

  • :eyes: You SHOULD validate all incoming data and return detailed error messages with appropriate HTTP status codes
  • :eyes: Error messages SHOULD be specific enough to guide the client toward fixing the issue
  • :eyes: Validation errors SHOULD return 400 Bad Request status code

Example:

HTTP/1.1 400 Bad Request Content-Type: application/problem+json Content-Language: en

{
 "type": "https://api.entur.io/v1/orders",
 "title": "Invalid input",
 "status": 400,
 "detail": "Field must be valid email.",
 "errors": [
   {
     "field": "email",
     "message": "Field must be valid email.",
     "code": "INVALID_FORMAT"
   }
 ]
}

6.7 HATEOAS

  • :eyes: You MAY include links to related resources in responses to enable easy API navigation

Example:

{
  "id": 123,
  "name": "Sentralstasjonen",
  "links": [
    { "rel": "self", "href": "/api/v1/bus-stops/123" },
    { "rel": "schedules", "href": "/api/v1/bus-stops/123/schedules" }
  ]
}

7. Alternative Approaches

7.1 When to Consider Non-RESTful Designs

There may be situations where a pure REST architecture is not the best solution. In such cases, consider alternative design patterns. The choice should be justified based on performance requirements, complexity, and consistency with other systems, as well as overall guidelines in the architect’s intent.

Alternative Design Patterns:

  • GraphQL: For flexible queries where the client can specify exactly what data is needed
  • Event-driven architecture: For asynchronous or event-based scenarios
  • WebSocket: For real-time bidirectional communication
  • gRPC: For high-performance, strongly-typed services

FAQ

Must existing APIs conform the guidelines?

  • Non-breaking changes (like adding example values) SHOULD be updated to be compliant with the guidelines.
  • Breaking changes MAY be added in a new version of the API.
  • New APIs MUST follow the guidelines.

Appendix

I. Breaking changes

A breaking change in an API is a change that “breaks” a clients integration with the API, without the client having made any changes on their end. The nature of breaking changes vary depending on the type of integration. For REST APIs, it is the OpenAPI specification that specifies the contract with the client. In practice, this means that if a client fetches the OpenAPI specification for a service and integrates with it, changes to the service that invalidates that specification, is considered breaking. In other words, changes should be backwards compatible with regards to the specification.

Examples of breaking changes:

  • Removal of a non-nullable field in a response (or making it nullable)
  • Adding a new required field in a request (or making a nullable field required)
  • Changing the physical datatype of a field, for example for string to number
  • Removal of an endpoint without following the deprecation rules
  • Adding new values to an enum property in a response. An enum is a contract specifying that a field will only have one of the following values. If the API starts to return new values, then the old specification is no longer valid, breaking backwards compatibility. See: 3.4 Enumerations.

However, an OpenAPI specification will never be a complete description of the behavior of an API. Some behavior will only be apparent when observing the output of the API, and clients will make assumptions based on the observed behavior. Examples:

  • The order of items in an array. A client may observe that elements in an array are sorted alphabetically and, even though this is not specified, assume that it will continue to be. Changing the sort order may then break a clients integration, even though the contract is not technically broken.
  • When exceptions are thrown. A client may observe that a given scenario returns an HTTP exception code, and assume that it will continue to do so, even though this is not explicitly documented in the specification.

These types of changes are harder to detect. They should be avoided, but sometimes clients may make unreasonable assumptions that can’t be taken into consideration. In the end, it is up to the API producer to judge whether a change should be considered breaking or not, and how to communicate the change to consumers. However, the amount of such breakages can be minimized through well-thought out and predictable API design that follows common guidelines.

II. Deprecation rules

At Entur, we should generally avoid making breaking changes. Instead, we should provide a deprecation period in which consumers of an API are given time to migrate to a new version. This period should be at least 12 months. The API producer is responsible for ensuring that their consumers are aware of the deprecation, but they can make this easier by following the guidelines for marking deprecation.


This site uses Just the Docs, a documentation theme for Jekyll.