Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions .spectral.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
extends: [spectral:oas]

functions:
- date
- requireExampleOrRef
- requireRequestBodyDescription

Expand Down Expand Up @@ -282,6 +283,89 @@ rules:
functionOptions:
match: ^[a-z0-9]+(-[a-z0-9]+)*$

# -------------------------------------------------------------------------
# 2.5 Lifecycle
# -------------------------------------------------------------------------

## On API level
entur-stability-level-api:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $.info.x-stability-level
then:
function: enumeration
functionOptions:
values: [draft, beta, stable]

entur-deprecation-api:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $.info.x-deprecated
then:
function: schema
functionOptions:
schema:
type: boolean

entur-sunset-format-api:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $[?(@property === "info" && @["x-deprecated"] === true)]
then:
field: x-sunset
function: date

entur-sunset-defined-api:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error # Or should this just be a warning?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if all teams can commit to a specific x-sunset date when they deprecate something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, might be. But also, maybe we should encourage setting it? The normal time frame is one year, so they could set that as a "default" and then push it later if they need to.

@egrimstad egrimstad Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make things more predictable for consumers. Seeing a deprecated API with no indication of sunsetting, feels a bit like it not being deprecated at all.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose in practice a team would have to book the x-sunset date into their calendar one year ahead of time, if they are going to execute it properly. Otherwise it will just be forgotten and picked back up again at some random point in time. So I feel like forcing x-sunset for deprecated resource won't really be meaningfully more accurate for consumers than the already well known 1 year deadline?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yes, this is a way to indicate the well known 1 year deadline. How would the consumers know that one year has passed since the deprecation happened? I suppose they could dig through the changelog to find it, but it seems much simpler to have it indicated in the specification itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And teams don't have to book it into the calendar, we can give them a nudge :D

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write a bit more about this in the guidelines?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely.

given: $[?(@property === "info" && @["x-deprecated"] === true)]
then:
- field: x-sunset
function: schema
functionOptions:
schema:
type: string

entur-sunset-undefined-api:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $[?(@property === "info" && @["x-deprecated"] !== true)]
then:
field: x-sunset
function: undefined

# On individual operation level
entur-stability-level-operation:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-stability-level
then:
function: enumeration
functionOptions:
values: [draft, beta, stable]

entur-sunset-format-operation:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-sunset
then:
function: date

entur-sunset-defined-operation:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#254-lifecycle"
severity: error # Or should this just be a warning?
given: $.paths.[?(@.deprecated === true)].x-sunset
then:
function: defined

entur-sunset-undefined-operation:
documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle"
severity: error
given: $.paths.[?(@.deprecated !== true)].x-sunset
message: "\"x-sunset\" should only be set when deprecated is true"
then:
function: "undefined"


# =============================================================================
# 3. Naming & Structure Conventions
Expand Down Expand Up @@ -363,12 +447,6 @@ rules:
# 3.2 Versioning
# -------------------------------------------------------------------------


# -------------------------------------------------------------------------
# 3.3 Backward Compatibility - Not directly lintable
# -------------------------------------------------------------------------


# =============================================================================
# 4. Communication Standards
# =============================================================================
Expand Down
14 changes: 14 additions & 0 deletions functions/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = (targetVal, options, context) => {
if (typeof targetVal !== "string") {
return [{ message: "Must be a valid calendar date."}]
}
if (targetVal.match(/^\d{4}-\d{2}-\d{2}$/) == null) {
return [{ message: "Must be a valid calendar date."}]
}

if (isNaN(Date.parse(targetVal))) {
return [{ message: "Must be a valid calendar date."}]
}

return []
}
74 changes: 67 additions & 7 deletions guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,67 @@ Here, only 1 specification will be shown on the Developer Portal, which is a com
</details>


### 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](https://spec.openapis.org/oas/v3.1.2.html#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 discontinued, 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](#I-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](#I-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](#I-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 `deprecated`/`x-deprecated` **MUST** have the value `true`
- :ballot_box_with_check: The field `x-sunset` **MUST** filled with a date that follows Enturs rules for deprecation.
- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` (or be empty)
- :ballot_box_with_check: You **MUST NOT** make [breaking changes](#I-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.

## 3. Naming & Structure Conventions
<!-- How to organize and label resources -->

Expand Down Expand Up @@ -292,12 +353,7 @@ published, the current year and month is used, and MICRO is set to "00". If a ve
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 Backward Compatibility
- :eyes: You **MUST** not remove or modify existing fields or endpoints
- :eyes: You **MUST** introduce new versions for changes that break previous contracts
- :eyes: You **MUST** clearly document which features are deprecated and provide guidance for migration

### 3.4 Tags Naming
### 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`
Expand Down Expand Up @@ -556,10 +612,14 @@ There may be situations where a pure REST architecture is not the best solution.
- **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
Here we should write about what we mean with breaking changes.
Loading