Full Stack • Java • System Design • Cloud • AI Engineering

Web Services2026-06-17

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

  1. Loose Coupling: Server can change URIs without breaking clients
  2. Discoverability: API is self-documenting through hypermedia links
  3. Flexibility: Business rules evolve on server side only
  4. State Management: Server controls workflow and state transitions
  5. Reduced Documentation: Links provide hints about available operations
  6. Scalability: Clients don't hardcode URIs or business logic
  7. Evolvability: API can evolve without versioning
  8. Client Simplicity: Clients follow links instead of constructing URIs

Best Practices

  1. Use Standard Link Relations: Use IANA registered link relations (self, next, prev)
  2. Include Self Links: Always include self link in resources
  3. State-Based Links: Only include links for valid state transitions
  4. Consistent Format: Use consistent hypermedia format (HAL, JSON:API)
  5. HTTP Methods: Specify HTTP method in links when not GET
  6. Error Responses: Include links in error responses for recovery
  7. Documentation: Document link relations and their meanings
  8. Versioning: Use hypermedia for API versioning when needed