Respectful REST APIs: ‘Sunset’ and ‘Deprecation’ HTTP Headers
Learn how hints can be transmitted to clients via the optional but useful Sunset and Deprecation HTTP Headers.
Join the DZone community and get the full member experience.
Join For Free1. Introduction
According to Richardson Maturity Model [Reference 1], a Level 3 REST architecture introduces discoverability through hypermedia controls in addition to resources and HTTP verbs, thus making communication between the involved actors more self-documenting.
Hypermedia enriches the interaction from various perspectives, decreasing the coupling between parties and also allowing them to evolve independently. Moreover, the data enclosed in the exchanged messages is enhanced with links, which makes the overall exchanged information more accurate. On the other hand, developers now need to pay more attention when thinking the design, as the representations have a greater impact.
HATEOAS (Hypermedia as the Engine of the Application State) is an architectural component that allows driving application state (resources’ representations) enhanced with hypermedia support.
Currently, the most common REST API implementations are those conforming to Level 2 (Reference 1), which (most of the time) fully solve the required business problems and thus are enough. A Level 3, on the other hand, provides more insights, as previously stated. Moreover, a REST API service provider can make a better promise (Reference 2) to its customers if for certain business use cases additional pieces of information (hints) are provided. If leveraged, these might make the contract between the two go even smoother.
This post describes how such hints can be transmitted to clients via the optional but useful Sunset and Deprecation HTTP Headers.
2. Context
Just as any other software product, APIs have the their own lifecycle (Reference 3). It is not uncommon for any target of a HTTP request that is a resource to no longer satisfy the requirements. While this may happen for various reasons, the concept is referred to as deprecation and means the resource is still operational although an alternative is available and recommended to be used instead. Usually, at some point, the former will reach its end of life (sunset) when it is decommissioned and finally removed.
No doubt, a process as brief as the one described above may be accommodated via documentation and announcements toward the clients so that the contract is not broken and the communication is up and running correctly. This is imperative and obviously all service providers will make sure they fulfill it. In addition, the Deprecation and Sunset HTTP headers may be used to help during this transition phase.
Moreover, apart from deprecation, the Sunset header alone helps handling some other resource lifecycle aspects that include temporary state, migration or retention (Reference 4).
This post aims to exemplify several use cases where the two headers are used in a Level 3 REST API. In this direction, a service provider adds optional but useful hints for its clients, out of courtesy.
3. Project
As part of this post, a small project that leverages Sunset and Deprecation HTTP headers is implemented. The purpose is to come closer to the service clients and provide them with more pieces of information that make the interaction more intuitive in case of the following technical situations:
- Temporary Resources Handling [Reference 4]
- Resources Retention [Reference 4]
- Resources Deprecation [References 4 and 5]
The project simulates a code review application where the domain is represented by a single entity – Review – that may be in a certain status throughout its lifecycle – DRAFT, OPEN, CLOSED, CANCELLED. In addition to attributes as id, status, and description, a review is described by the following date fields: dateCreated, dateOpened, dateClosed, and dateCancelled.
A REST controller uses a service that further accesses the entities via a Spring Data JPA repository on top of an in-memory H2 database table.
The exposed operations are:
- Retrieve all reviews:
GET /reviews
- Retrieve one review:
GET /reviews/{id}
- Search reviews:
GET /reviews/search?filter=pattern
The exchanged messages include hypermedia and are HAL (Hypertext Application Language) formatted.
4. Use Cases to Analyze
As already mentioned above, apart from the normal implementation, we focus on the following three business use cases:
- A DRAFT review is a newly created one that has not been OPEN yet. If not opened within two days, it will be permanently deleted.
Technically, GET /review/{id}
behaves differently before and after the moment designated by (dateCreated + 2 days):
- before – the DRAFT review is returned – 200 OK
- after – the DRAFT review is not returned anymore – 404 Not Found
- A recently created review (DRAFT) may be then either CLOSED or CANCELLED. If CANCELLED, it will be kept in the system for 1 year and then permanently deleted.
Again, technically, GET /review/{id}
behaves differently before and after the moment designated by (dateCancelled
+ 1 year):
- before – the CANCELLED review is returned – 200 OK
- after – the CANCELLED review is not returned anymore – 404 Not Found
- At some point, for specific reasons, the product team decides GET /reviews endpoint will be removed. This will happen gradually, in two stages.
- First, by stating it is not the preferred way to retrieve all reviews and recommending favoring
GET /reviews/search
instead. - Then, by effectively decommissioning and removing it.
- First, by stating it is not the preferred way to retrieve all reviews and recommending favoring
Technically, the two endpoints behave differently before and after the date of the first stage (let it be date1) and the date of the second stage (let it be date2) respectively:
- Before
date1
: BothGET /reviews
andGET /reviews/search
are usable (200 OK) - After
date1
, but beforedate2
: Again, bothGET /reviews
andGET /reviews/search
are usable (200 OK) - After
date2
:GET /reviews
will not work anymore (404 Not Found), whileGET /reviews/search
will continue to be usable (200 OK)
Each of the three represents a technical use case as described in [Reference 4] and [Reference 5].
5. Solution Design
This section outlines how the previously mentioned use cases may be addressed not only via documentation, but also with additional “hints” that may help the clients.
- Temporary Resource Handling: The response of each GET /review/{id} in case of a DRAFT review will contain the ‘Sunset’ header.
xxxxxxxxxx
Sunset: dateCreated + 2 days
If interpreted, the header informs the client the resource will not be available anymore after the ‘Sunset’ date.
- Resource Retention: The response of each
GET /review/{id}
in case of a CANCELLED review will contain the ‘Sunset’ header.
xxxxxxxxxx
Sunset: dateCancelled + 1 year
If interpreted, the header informs the client the resource will not be available anymore after the ‘Sunset’ date.
- Resource Deprecation
- First stage: The response of
GET /reviews
will contain the ‘Deprecation’ and ‘Link’ headers.
xxxxxxxxxx
Deprecation: true
Link: http://reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
If interpreted, the two headers inform the client the endpoint is considered deprecated as well as provide an alternative and also a way of finding more about this decision.
Or:
Deprecation: 01 Jan 2021 00:00:00 GMT
Link: http://reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
If interpreted, the two headers inform the client the endpoint is considered deprecated since 01 Jan 2021 00:00:00 GMT, provide an alternative and also a way of finding more about this decision.
- Second stage: The response of
GET /reviews
will additionally contain the ‘Sunset’ header:
xxxxxxxxxx
Deprecation: true
Link: http://reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
Sunset: 31 Dec 2021 23:59:59 GMT
If interpreted, the three headers inform the client the endpoint is considered deprecated, provide an alternative and also a way of finding more about this decision. Moreover, they say the endpoint is responsive until the ‘Sunset’ date and then decommissioned.
Or:
xxxxxxxxxx
Deprecation: 01 Jan 2021 00:00:00 GMT
Link: http://reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
Sunset: 31 Dec 2021 23:59:59 GMT
If interpreted, the three headers inform the client the endpoint is considered deprecated since 01 Jan 2021 00:00:00 GMT, provide an alternative and also a way of finding more about this decision. Moreover, they say the endpoint is responsive until the ‘Sunset’ date and then decommissioned.
6. Project Implementation
‘sunsetheader’ is the sample project, especially developed for the purpose of this post. It uses the following:
- Java 11
- Spring Boot 2.4.1
- Spring HATEOAS 1.2.2
- Apache Maven 3.6.3
Since it designates a code review application, the domain is represented by a Review entity. A minimum number of attributes are declared, the most significant ones being status and the dates that actually are milestones of a status change.
public class Review {
private Long id;
private String description;
private Status status;
private LocalDateTime dateCreated;
private LocalDateTime dateOpened;
private LocalDateTime dateClosed;
private LocalDateTime dateCancelled;
public Review(Status status,
String description, LocalDateTime dateCreated) {
this.description = description;
this.status = status;
this.dateCreated = dateCreated;
}
public enum Status {
DRAFT, OPEN, CLOSED, CANCELLED;
}
}
Since this is a sample project, a few reviews are ingested (one for each status) right at startup via a CommandLineRunner
.
xxxxxxxxxx
CommandLineRunner initData(ReviewRepository repository) {
return args -> {
Review draft = new Review(Status.DRAFT, "Draft review.", LocalDateTime.now());
Review open = new Review(Status.OPEN, "Open review.", LocalDateTime.now());
open.setDateOpened(open.getDateCreated().plusMinutes(5));
Review closed = new Review(Status.CLOSED, "Closed review.", LocalDateTime.now());
closed.setDateOpened(closed.getDateCreated().plusMinutes(10));
closed.setDateClosed(closed.getDateOpened().plusDays(3));
Review cancelled = new Review(Status.CANCELLED, "Cancelled review.", LocalDateTime.now());
cancelled.setDateOpened(cancelled.getDateCreated().plusMinutes(15));
cancelled.setDateCancelled(cancelled.getDateCreated().plusMonths(1));
List.of(draft, open, closed, cancelled)
.forEach(review -> log.info("Load " + repository.save(review)));
};
}
As specified in section 3, the implemented operations are exposed from the ReviewController
.
xxxxxxxxxx
public class ReviewController {
private final ReviewService service;
private final ReviewModelAssembler assembler;
public ReviewController(ReviewService service,
ReviewModelAssembler assembler) {
this.service = service;
this.assembler = assembler;
}
("/reviews")
public ResponseEntity<?> all(HttpServletResponse response) {
final List<Review> reviews = service.findAll();
List<EntityModel<Review>> content = reviews.stream()
.map(assembler::toModel)
.collect(toList());
Link link = linkTo(methodOn(getClass()).all(response)).withSelfRel();
return ResponseEntity.ok()
.body(of(content, link));
}
("/reviews/{id}")
public ResponseEntity<?> one(HttpServletResponse response, Long id) {
try {
Review review = service.findOne(id);
return ResponseEntity.ok(assembler.toModel(review));
} catch (EntityNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
("/reviews/search")
public ResponseEntity<?> search( (name = "filter", required = true) String filter) {
final List<Review> reviews = service.search(filter);
List<EntityModel<Review>> content = reviews.stream()
.map(assembler::toModel)
.collect(toList());
Link link = linkTo(methodOn(getClass()).search(filter)).withSelfRel();
return ResponseEntity.ok()
.body(of(content, link));
}
}
Since a Level 3 REST API is intended, a RepresentationModelAssembler
is used to help in achieving it. One may also observe the status transitions a Review is permitted to go through.
xxxxxxxxxx
public class ReviewModelAssembler implements RepresentationModelAssembler<Review, EntityModel<Review>> {
public EntityModel<Review> toModel(Review entity) {
EntityModel<Review> model = EntityModel.of(entity);
model.add(linkTo(methodOn(ReviewController.class).one(null, entity.getId())).withSelfRel(),
linkTo(methodOn(ReviewController.class).all(null)).withRel("reviews"),
linkTo(methodOn(ReviewController.class).search("pattern")).withRel("search"));
if (entity.getStatus() == Status.DRAFT) {
model.add(linkTo(methodOn(ReviewController.class).open(entity.getId())).withRel("open"));
} else if (entity.getStatus() == Status.OPEN) {
model.add(linkTo(methodOn(ReviewController.class).close(entity.getId())).withRel("close"));
model.add(linkTo(methodOn(ReviewController.class).cancel(entity.getId())).withRel("cancel"));
}
return model;
}
}
The implementation is straight-forward, nothing out of the ordinary. If interested in other particular implemented components (service or repository), one may check directly the project source code (see Resources).
7. Solution Implementation
Basically, sections 4 and 5 describe the goals of this post while this describes a way of actually implementing them.
Temporary Resource & Resource Retention
From a solution point of view, both temporary resource handling and resource retention use cases are implemented in the same manner, by leveraging the ‘Sunset’ header. The only difference is what its value represents, as described in section 5.
One may consider such hints are worth mentioning not only in the header, but also as part of the response body itself. For the sake of clarity, the focus in this post is solely on the headers.
Since a Review is a resource that in a certain status may be either temporary (DRAFT) or disposable at some point (CANCELLED), one may say it is ‘Sunsetable’. As such, the following interface is defined and a Review is made to be Sunsetable.
xxxxxxxxxx
public interface Sunsetable {
Optional<LocalDateTime> sunsetDate();
}
The interface defines a single method that provides a sunset date, if available.
xxxxxxxxxx
public class Review implements Sunsetable {
...
public Optional<LocalDateTime> sunsetDate() {
LocalDateTime result = null;
if (status == Status.DRAFT) {
result = dateCreated.plusDays(2);
} else if (status == Status.CANCELLED) {
result = dateCancelled.plusYears(1);
}
return Optional.ofNullable(result);
}
...
}
It may be easily observed that a DRAFT Review sunsets two days after its creation, while a CANCELLED one, 1 year after it has been cancelled.
So far, the optional and dynamic sunsetDate
attribute was attached to a Review. The next step is to make this hint visible to a client requesting a Review.
In order to keep the concern of retrieving a Review and the one that enriches the response with the designated ‘Sunset’ HTTP header decoupled, the implementation of GET /reviews/{id}
operation is enhanced to publish an application event once the entity is successfully retrieved and ready to be sent.
xxxxxxxxxx
"/reviews/{id}") (
public ResponseEntity<?> one(HttpServletResponse response, Long id) {
try {
Review review = service.findOne(id);
eventPublisher.publishEvent(new SunsetEvent(this,
response, review.sunsetDate()));
return ResponseEntity.ok(assembler.toModel(review));
} catch (EntityNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
That is all that needs to be changed at the Controller level (of course, apart from the injection the ApplicationEventPublisher
dependency). The early mentioned application event contains the sunsetDate
, if available.
xxxxxxxxxx
public class SunsetEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final HttpServletResponse response;
private final Optional<LocalDateTime> value;
public SunsetEvent(Object source,
HttpServletResponse response, Optional<LocalDateTime> value) {
super(source);
this.response = response;
this.value = value;
}
}
The last step is to create an ApplicationListener
that waits for such events. In case one is received, the ‘Sunset’ HTTP header value is set and visible to clients.
xxxxxxxxxx
class SunsetEventListener extends AbstractEventListener<SunsetEvent> {
public void onApplicationEvent(SunsetEvent event) {
Optional<LocalDateTime> date = event.getValue();
if (date.isPresent()) {
event.getResponse().addHeader("Sunset", format(date.get()));
}
}
}
Examples:
Response Header contains:
xxxxxxxxxx
Sunset: 21 Jan 2021 15:02:29 GMT
Response body:
xxxxxxxxxx
{
"id": 1,
"description": "Draft review.",
"status": "DRAFT",
"dateCreated": "2021-01-19T15:02:29.503462",
"dateOpened": null,
"dateClosed": null,
"dateCancelled": null,
"_links": {
"self": {
"href": "http://localhost:8080/reviews/1"
},
"reviews": {
"href": "http://localhost:8080/reviews"
},
"search": {
"href": "http://localhost:8080/reviews/search?filter=pattern"
},
"open": {
"href": "http://localhost:8080/reviews/1/open"
}
}
}
The client is informed this Review is available until 21 Jan 2021 15:02:29 GMT (two days after it was created), which means this entity is in a temporary state.
- Request a
CANCELLED
review –GET http://localhost:8080/reviews/4
.
Response Header contains:
xxxxxxxxxx
Sunset: 19 Feb 2022 15:02:29 GMT
Response body:
xxxxxxxxxx
{
"id": 4,
"description": "Cancelled review.",
"status": "CANCELLED",
"dateCreated": "2021-01-19T15:02:29.504462",
"dateOpened": "2021-01-19T15:17:29.504462",
"dateClosed": null,
"dateCancelled": "2021-02-19T15:02:29.504462",
"_links": {
"self": {
"href": "http://localhost:8080/reviews/4"
},
"reviews": {
"href": "http://localhost:8080/reviews"
},
"search": {
"href": "http://localhost:8080/reviews/search?filter=pattern"
}
}
}
The client is informed this Review is available until 19 Feb 2022 15:02:29 GMT (one year after it was cancelled), which means this entity is in a temporary state. Also, a hint on the retention policy of such entities is provided.
To conclude, whenever a Sunsetable entity is retrieved, a Sunset application event is published containing the sunset date, if available. Then, the corresponding ApplicationListener is the one that actually adds the ‘Sunset’ HTTP header to the response, if present.
Resource Deprecation
As already specified in section 4, the resource that designates the operation of retrieving all Reviews is considered deprecated. On the service side, this is marked through a custom annotation – @DeprecatedResource
. See (Reference 6) for the Resource definition.
xxxxxxxxxx
ElementType.METHOD}) ({
RetentionPolicy.RUNTIME) (
public @interface DeprecatedResource {
String since() default "";
String alternate() default "";
String policy() default "";
String sunset() default "";
}
since
: The date since the resource is declared and considered deprecatedalternate
: The suggested alternative (either a new version of the resource, or another resource)policy
: The deprecation policysunset
: The date the resourced is decommissioned and becomes unavailable
Throughout the former stage of the deprecation process (we have already mentioned that usually there are two), the service implementer annotates the method as follows and the intent is clear on this side.
xxxxxxxxxx
since = "01 Jan 2021 00:00:00 GMT", (
alternate = "/reviews/search?filter=pattern",
policy = "https://technically-correct.eu/deprecation-policy")
"/reviews") (
public ResponseEntity<?> all(HttpServletResponse response) {
final List<Review> reviews = service.findAll();
List<EntityModel<Review>> content = reviews.stream()
.map(assembler::toModel)
.collect(toList());
Link link = linkTo(methodOn(getClass()).all(response)).withSelfRel();
return ResponseEntity.ok()
.body(of(content, link));
}
It should be noted that at this point, the ‘sunset’ value is not provided, as this information most likely is not known. Once available, the latter stage of the process starts and the method is annotated as below.
xxxxxxxxxx
since = "01 Jan 2021 00:00:00 GMT", (
alternate = "/reviews/search?filter=pattern",
policy = "https://technically-correct.eu/deprecation-policy",
sunset = "31 Dec 2021 23:59:59 GMT")
The next step is to make this hint visible to a client calling this endpoint.
As previously, the concern of retrieving all Reviews and the one that enriches the response with the designated ‘Deprecation’, ‘Link’ and potentially ‘Sunset’ HTTP headers are kept decoupled. The implementation is enhanced so that @DeprecatedResource
annotated endpoints are intercepted. An interceptor is defined to accommodate this.
xxxxxxxxxx
public class DeprecatedResourceInterceptor implements HandlerInterceptor {
private final ApplicationEventPublisher eventPublisher;
public DeprecatedResourceInterceptor(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
DeprecatedResource deprecated = handlerMethod.getMethod()
.getAnnotation(DeprecatedResource.class);
if (deprecated == null) {
return true;
}
var event = new DeprecatedResourceEvent(this, request, response,
deprecated.since(), deprecated.alternate(),
deprecated.policy(), deprecated.sunset());
eventPublisher.publishEvent(event);
}
return true;
}
}
In case a @DeprecatedResource
annotated handler method is called, an ApplicationEvent
containing pieces of information related to its deprecation is published.
xxxxxxxxxx
public class DeprecatedResourceEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final HttpServletRequest request;
private final HttpServletResponse response;
private final String since;
private final String alternate;
private final String policy;
private final String sunset;
public DeprecatedResourceEvent(Object source,
HttpServletRequest request, HttpServletResponse response,
String since, String alternate, String policy,
String sunset) {
super(source);
this.request = request;
this.response = response;
this.since = since;
this.alternate = alternate;
this.policy = policy;
this.sunset = sunset;
}
}
The last step is to create an ApplicationListener
that receives such events and adds the HTTP headers accordingly in order to be visible to the clients.
xxxxxxxxxx
class DeprecatedResourceEventListener extends AbstractEventListener<DeprecatedResourceEvent> {
private final ApplicationEventPublisher eventPublisher;
public DeprecatedResourceEventListener(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void onApplicationEvent(DeprecatedResourceEvent event) {
event.getResponse().addHeader("Deprecation", deprecation(event));
event.getResponse().addHeader(HttpHeaders.LINK, link(event));
eventPublisher.publishEvent(new SunsetEvent(this,
event.getResponse(), parse(event.getSunset())));
}
private String deprecation(DeprecatedResourceEvent event) {
Optional<LocalDateTime> since = parse(event.getSince());
if (since.isPresent()) {
return event.getSince();
}
return String.valueOf(true);
}
private String link(DeprecatedResourceEvent event) {
return formatLink(contextPath(event.getRequest()) + event.getAlternate(), "alternate") + "," +
formatLink(event.getPolicy(), "deprecation");
}
...
}
A few notes are needed.
The value of the ‘Deprecation’ is either the boolean true value or a date, in case the ‘since’ attribute is provided and valid. The value of the ‘Link’ contains two pieces of information – the alternate that is to be favored and the deprecation policy. Moreover, a SunsetEvent
(exactly as described in the previous subsection) is published.
Throughout the first stage, nothing more will appear in the headers’ section. In case the second phase of the process is reached, a ‘sunset’ value is provided at annotation level and consequently available when the SunsetEvent
is published. Thus, a value for the ‘Sunset’ header is added in the response. See the implementation of the SunsetEventListener
as presented above.
Examples:
- Request all reviews –
GET http://localhost:8080/reviews
, during the first stage of the deprecation process
Response Header contains:
xxxxxxxxxx
Deprecation: true
Link: http://localhost8080/reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
Or:
xxxxxxxxxx
Deprecation: 01 Jan 2021 00:00:00 GMT
Link: http://localhost8080/reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
...depending on the provisioning of the ‘since’ information.
The client is informed that GET /reviews operation is considered deprecated (since 01 Jan 2021 00:00:00 GMT), encouraged to use /reviews/search?filter=pattern
instead and informed about the deprecation policy available at https://technically-correct.eu/deprecation-policy.
- Request all reviews:
GET http://localhost:8080/reviews
during the second stage of the deprecation process
Response Header contains
xxxxxxxxxx
Deprecation: true
Link http://localhost8080/reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
Sunset: 31 Dec 2021 23:59:59 GMT
Or:
xxxxxxxxxx
Deprecation: 01 Jan 2021 00:00:00 GMT
Link http://localhost8080/reviews/search?filter=pattern; rel="alternate",https://technically-correct.eu/deprecation-policy; rel="deprecation"
Sunset: 31 Dec 2021 23:59:59 GMT
...depending on the provisioning of the ‘since’ information.
The client is informed that the GET /reviews
operation is considered deprecated (since 01 Jan 2021 00:00:00 GMT), encouraged to use /reviews/search?filter=pattern
instead, and informed about the deprecation policy available at https://technically-correct.eu/deprecation-policy. Moreover, it is communicated the operation is still available until 31 Dec 2021 23:59:59 GMT when it is decommissioned.
It is worth observing, that the value of the Deprecation header is either Boolean or Date. This is not something I am very fond of; I would have preferred it either or. On the other hand though, if seen as a best effort, I think it is acceptable.
8. Conclusion
When implementing APIs (in particular REST APIs), the most important aspect is to keep the “promise” to the clients using it — that is, to never break the contract. This is mandatory. In addition to correctness, at another level, come the nice-to-have features that a service provider might add so that the clients have an easier and more pleasant use of the service. In this post, two such features are presented – ‘Sunset’ and ‘Deprecation’ HTTP headers – courteous, respectful features that decorate the contract between a client and a server.
Apart from the theoretical aspects, an actual implementation was presented in order to make these concepts more clear.
References
- “Richardson Maturity Model”, by Martin Fowler, March 18th, 2010 – https://martinfowler.com/articles/richardsonMaturityModel.html
- “An API is a promise”, by Erik Wilde, December 16th, 2020 – https://apifriends.com/api-management/an-api-is-a-promise/
- “API Lifecycle Management: Deprecation and Sunsetting”, by Erik Wilde, November 16th, 2020 – https://apifriends.com/api-management/api-lifecycle-management-deprecation-and-sunsetting/
- RFC8594 – The Sunset HTTP Header Field – https://tools.ietf.org/html/rfc8594
- draft-dalal-deprecation-header-03 – The Deprecation HTTP Header Field – https://tools.ietf.org/html/draft-dalal-deprecation-header-03
- Section 2 of RFC7231 – Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content – https://tools.ietf.org/html/rfc7231#section-2
Resources
The fully functional sample project is available in GitHub – sunsetheader.
The picture was taken in 2007, while crossing the Adriatic Sea, from Brindisi to Igoumenitsa.
Published at DZone with permission of Horatiu Dan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments