RESTful Web Services & HATEOAS
Master RESTful APIs with visual diagrams covering REST principles, HTTP methods, HATEOAS, Richardson Maturity Model, and complete implementation examples
REST (Representational State Transfer) is an architectural style for designing networked applications. HATEOAS (Hypermedia as the Engine of Application State) represents the highest level of REST maturity, enabling truly discoverable and self-documenting APIs.
Richardson Maturity Model
graph TB
A[Level 0: The Swamp of POX] --> B[Level 1: Resources]
B --> C[Level 2: HTTP Verbs]
C --> D[Level 3: Hypermedia Controls HATEOAS]
A1[Single URI<br/>Single Method<br/>XML/JSON over HTTP] --> A
B1[Multiple URIs<br/>Resource-based<br/>Still single method] --> B
C1[HTTP Methods<br/>GET POST PUT DELETE<br/>Status Codes] --> C
D1[Hypermedia Links<br/>Discoverable API<br/>State Transitions] --> D
style D fill:#4CAF50
style C fill:#FF9800
style B fill:#2196F3
style A fill:#9E9E9E
Key Points:
- Level 0: Single endpoint, single HTTP method (usually POST), RPC-style
- Level 1: Multiple URIs for different resources, resource-oriented
- Level 2: Proper use of HTTP verbs and status codes
- Level 3: HATEOAS - hypermedia controls for API discoverability
- Maturity: Each level builds upon the previous, Level 3 is true REST
REST Maturity Levels Code Examples
// Level 0: The Swamp of POX (Plain Old XML)
// Single endpoint, everything via POST
POST /api/service
{
"action": "getBook",
"bookId": 1234
}
POST /api/service
{
"action": "createBook",
"title": "Java Guide",
"author": "John Doe"
}
// Level 1: Resources
// Multiple URIs, resource-based, but still using POST for everything
POST /api/books/1234/get
POST /api/books/create
{
"title": "Java Guide",
"author": "John Doe"
}
// Level 2: HTTP Verbs
// Proper use of HTTP methods and status codes
GET /api/books/1234
// Response: 200 OK
POST /api/books
{
"title": "Java Guide",
"author": "John Doe"
}
// Response: 201 Created
PUT /api/books/1234
{
"title": "Java Guide Updated",
"author": "John Doe"
}
// Response: 200 OK
DELETE /api/books/1234
// Response: 204 No Content
// Level 3: HATEOAS
// Includes hypermedia links for state transitions
GET /api/books/1234
{
"id": 1234,
"title": "Java Guide",
"author": "John Doe",
"_links": {
"self": {
"href": "http://api.example.com/books/1234"
},
"reviews": {
"href": "http://api.example.com/books/1234/reviews"
},
"author": {
"href": "http://api.example.com/authors/567"
},
"purchase": {
"href": "http://api.example.com/orders/books/1234"
}
}
}
HATEOAS Architecture
sequenceDiagram
participant Client
participant API as REST API
participant DB as Database
Client->>API: GET /api/books/1234
API->>DB: Query book
DB-->>API: Book data
API-->>Client: Book + Hypermedia Links
Note over Client: Client discovers<br/>available actions<br/>from links
Client->>API: Follow "reviews" link
API->>DB: Query reviews
DB-->>API: Reviews data
API-->>Client: Reviews + Links
Client->>API: Follow "purchase" link
API->>DB: Create order
DB-->>API: Order created
API-->>Client: Order + Links (status, cancel)
Note over Client,API: State transitions<br/>driven by server<br/>via hypermedia
Key Points:
- Discoverability: Client discovers available actions from hypermedia links
- Loose Coupling: Server can change URIs without breaking clients
- State Transitions: Server controls workflow through links
- Self-Documentation: Links provide hints about available operations
- Flexibility: Business rules evolve on server without client changes
HATEOAS Implementation
// 1. Book Entity
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
private String isbn;
private Double price;
private BookStatus status; // AVAILABLE, CHECKED_OUT, RESERVED
// Getters and setters
}
// 2. Book Resource with HATEOAS (Spring HATEOAS)
@RestController
@RequestMapping("/api/books")
public class BookController {
@Autowired
private BookService bookService;
// GET single book with HATEOAS links
@GetMapping("/{id}")
public EntityModel<Book> getBook(@PathVariable Long id) {
Book book = bookService.findById(id);
// Create resource with self link
EntityModel<Book> resource = EntityModel.of(book);
// Add self link
resource.add(linkTo(methodOn(BookController.class)
.getBook(id)).withSelfRel());
// Add related links based on book state
if (book.getStatus() == BookStatus.AVAILABLE) {
// Can checkout if available
resource.add(linkTo(methodOn(BookController.class)
.checkoutBook(id)).withRel("checkout"));
// Can reserve if available
resource.add(linkTo(methodOn(BookController.class)
.reserveBook(id)).withRel("reserve"));
}
if (book.getStatus() == BookStatus.CHECKED_OUT) {
// Can return if checked out
resource.add(linkTo(methodOn(BookController.class)
.returnBook(id)).withRel("return"));
}
if (book.getStatus() == BookStatus.RESERVED) {
// Can cancel reservation
resource.add(linkTo(methodOn(BookController.class)
.cancelReservation(id)).withRel("cancel-reservation"));
}
// Always add reviews link
resource.add(linkTo(methodOn(ReviewController.class)
.getBookReviews(id)).withRel("reviews"));
// Add author link
resource.add(linkTo(methodOn(AuthorController.class)
.getAuthor(book.getAuthorId())).withRel("author"));
return resource;
}
// GET all books with pagination
@GetMapping
public CollectionModel<EntityModel<Book>> getAllBooks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Page<Book> bookPage = bookService.findAll(
PageRequest.of(page, size));
List<EntityModel<Book>> books = bookPage.getContent().stream()
.map(book -> EntityModel.of(book,
linkTo(methodOn(BookController.class)
.getBook(book.getId())).withSelfRel()))
.collect(Collectors.toList());
CollectionModel<EntityModel<Book>> collection =
CollectionModel.of(books);
// Add pagination links
collection.add(linkTo(methodOn(BookController.class)
.getAllBooks(page, size)).withSelfRel());
if (bookPage.hasNext()) {
collection.add(linkTo(methodOn(BookController.class)
.getAllBooks(page + 1, size)).withRel("next"));
}
if (bookPage.hasPrevious()) {
collection.add(linkTo(methodOn(BookController.class)
.getAllBooks(page - 1, size)).withRel("prev"));
}
return collection;
}
// POST - Create new book
@PostMapping
public ResponseEntity<EntityModel<Book>> createBook(
@RequestBody Book book) {
Book savedBook = bookService.save(book);
EntityModel<Book> resource = EntityModel.of(savedBook);
resource.add(linkTo(methodOn(BookController.class)
.getBook(savedBook.getId())).withSelfRel());
return ResponseEntity
.created(resource.getRequiredLink("self").toUri())
.body(resource);
}
// State transition methods
@PostMapping("/{id}/checkout")
public EntityModel<Book> checkoutBook(@PathVariable Long id) {
Book book = bookService.checkout(id);
return getBook(id); // Returns book with updated links
}
@PostMapping("/{id}/return")
public EntityModel<Book> returnBook(@PathVariable Long id) {
Book book = bookService.returnBook(id);
return getBook(id);
}
@PostMapping("/{id}/reserve")
public EntityModel<Book> reserveBook(@PathVariable Long id) {
Book book = bookService.reserve(id);
return getBook(id);
}
}
HATEOAS Response Examples
graph TB
subgraph "Book Available State"
A[GET /books/1234] --> B[Book Resource]
B --> C[self link]
B --> D[checkout link]
B --> E[reserve link]
B --> F[reviews link]
B --> G[author link]
end
subgraph "Book Checked Out State"
H[GET /books/1234] --> I[Book Resource]
I --> J[self link]
I --> K[return link]
I --> L[reviews link]
I --> M[author link]
end
style B fill:#4CAF50
style I fill:#FF9800
Key Points:
- State-Dependent Links: Available actions change based on resource state
- Self Link: Always includes link to itself
- Related Resources: Links to associated resources (reviews, author)
- Actions: Links represent possible state transitions
- Discovery: Client doesn't need to construct URIs
JSON Response Examples
// Available Book - Full options
{
"id": 1234,
"title": "Java Interview Guide",
"author": "John Doe",
"isbn": "978-1234567890",
"price": 49.99,
"status": "AVAILABLE",
"_links": {
"self": {
"href": "http://api.example.com/api/books/1234"
},
"checkout": {
"href": "http://api.example.com/api/books/1234/checkout",
"method": "POST"
},
"reserve": {
"href": "http://api.example.com/api/books/1234/reserve",
"method": "POST"
},
"reviews": {
"href": "http://api.example.com/api/books/1234/reviews"
},
"author": {
"href": "http://api.example.com/api/authors/567"
}
}
}
// Checked Out Book - Limited options
{
"id": 1234,
"title": "Java Interview Guide",
"author": "John Doe",
"isbn": "978-1234567890",
"price": 49.99,
"status": "CHECKED_OUT",
"dueDate": "2026-07-01",
"_links": {
"self": {
"href": "http://api.example.com/api/books/1234"
},
"return": {
"href": "http://api.example.com/api/books/1234/return",
"method": "POST"
},
"reviews": {
"href": "http://api.example.com/api/books/1234/reviews"
},
"author": {
"href": "http://api.example.com/api/authors/567"
}
}
}
// Collection with Pagination
{
"_embedded": {
"books": [
{
"id": 1234,
"title": "Java Interview Guide",
"_links": {
"self": {
"href": "http://api.example.com/api/books/1234"
}
}
},
{
"id": 1235,
"title": "Spring Boot Guide",
"_links": {
"self": {
"href": "http://api.example.com/api/books/1235"
}
}
}
]
},
"_links": {
"self": {
"href": "http://api.example.com/api/books?page=0&size=10"
},
"next": {
"href": "http://api.example.com/api/books?page=1&size=10"
},
"last": {
"href": "http://api.example.com/api/books?page=5&size=10"
}
},
"page": {
"size": 10,
"totalElements": 52,
"totalPages": 6,
"number": 0
}
}
Benefits of HATEOAS
- Loose Coupling: Server can change URIs without breaking clients
- Discoverability: API is self-documenting through hypermedia links
- Flexibility: Business rules evolve on server side only
- State Management: Server controls workflow and state transitions
- Reduced Documentation: Links provide hints about available operations
- Scalability: Clients don't hardcode URIs or business logic
- Evolvability: API can evolve without versioning
- Client Simplicity: Clients follow links instead of constructing URIs
Best Practices
- Use Standard Link Relations: Use IANA registered link relations (self, next, prev)
- Include Self Links: Always include self link in resources
- State-Based Links: Only include links for valid state transitions
- Consistent Format: Use consistent hypermedia format (HAL, JSON:API)
- HTTP Methods: Specify HTTP method in links when not GET
- Error Responses: Include links in error responses for recovery
- Documentation: Document link relations and their meanings
- Versioning: Use hypermedia for API versioning when needed