hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses REST API Design with @hectoday/http

What Makes an API RESTful

  • APIs are contracts
  • Project setup
  • Resources, not actions

HTTP Methods

  • GET, POST, PUT, PATCH, DELETE
  • Idempotency
  • Method safety and side effects

Status Codes

  • The status codes that matter
  • Error responses

Resource Design

  • Modeling resources
  • Partial responses and field selection
  • Pagination
  • Filtering, sorting, and searching

API Lifecycle

  • Versioning
  • Content negotiation
  • Rate limiting and quotas

Advanced Patterns

  • Bulk operations
  • Long-running operations
  • HATEOAS and discoverability

Putting It All Together

  • API design checklist
  • Summary

HATEOAS and discoverability

What if your API described itself?

When you browse a website, you don’t memorize URLs. You click links. Each page tells you where you can go next. HATEOAS (Hypermedia As The Engine Of Application State) is the same idea applied to APIs: responses should include links to related resources and available actions.

The name is a mouthful, but the concept is simple. When a client fetches a book, the response should tell them: here’s where the author is, here’s where the reviews are, and here’s how to update or delete this book.

Links in responses

{
  "id": "book-1",
  "title": "The Old Man and the Sea",
  "genre": "fiction",
  "authorId": "author-1",
  "_links": {
    "self": { "href": "/books/book-1" },
    "author": { "href": "/authors/author-1" },
    "reviews": { "href": "/books/book-1/reviews" },
    "update": { "href": "/books/book-1", "method": "PATCH" },
    "delete": { "href": "/books/book-1", "method": "DELETE" }
  }
}

The _links object is a map of related resources. Each key describes the relationship (self, author, reviews), and the value contains the URL and optionally the HTTP method.

A developer can look at this response and know exactly what they can do next without consulting any documentation.

Implementation

function addBookLinks(book: any): any {
  return {
    ...book,
    _links: {
      self: { href: `/books/${book.id}` },
      author: { href: `/authors/${book.authorId}` },
      reviews: { href: `/books/${book.id}/reviews` },
      update: { href: `/books/${book.id}`, method: "PATCH" },
      delete: { href: `/books/${book.id}`, method: "DELETE" },
    },
  };
}

route.get("/books/:id", {
  request: { params: z.object({ id: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
    const { id } = c.input.params;
    const book = books.find((b) => b.id === id);
    if (!book) return notFound("Book");
    return Response.json(addBookLinks(book));
  },
});

The addBookLinks function takes a book object and adds the _links property. Every book gets the same set of links. The URLs are constructed from the book’s data, so they’re always correct.

Pagination links

Where HATEOAS really shines is in paginated responses. Without links, the client has to construct the next page URL themselves, figuring out which parameter to use and how to encode the cursor. With links, they just follow the URL:

{
  "data": [ ... ],
  "pagination": {
    "limit": 20,
    "nextCursor": "abc123",
    "hasMore": true
  },
  "_links": {
    "self": { "href": "/books?limit=20" },
    "next": { "href": "/books?limit=20&cursor=abc123" }
  }
}

The client doesn’t need to know how cursors work. They just follow _links.next to get the next page. If there’s no next link, they’ve reached the end.

When HATEOAS helps

API exploration. A developer can start at GET / and follow links to discover the entire API. No documentation needed just to figure out what endpoints exist.

Decoupling. If you change your URL structure (say, from /books/123 to /api/v2/books/123), clients that follow links adapt automatically. Clients that hardcode URLs break.

Conditional actions. Links can appear or disappear based on permissions. If the user can’t delete a book, the delete link simply isn’t there. The client can use the presence of the link to decide whether to show a delete button.

When HATEOAS is overkill

Most APIs, honestly. In practice, API clients are built by reading documentation and hardcoding URL patterns. They don’t discover endpoints dynamically by following links. Adding _links to every response adds payload size without being consumed.

Internal APIs. When you control both the client and the server, link discovery is unnecessary. You already know the URLs because you wrote both sides.

Simple APIs. A CRUD API with five endpoints doesn’t need navigation links. The documentation page is sufficient.

The pragmatic approach

Full HATEOAS is rare in production APIs. But partial HATEOAS is useful. Here’s what to consider:

Always include: pagination links (next, prev). These genuinely save the client from URL construction, especially when pagination parameters are complex.

Sometimes include: self links (self). Useful when a resource appears in a list and the client needs the canonical URL.

Skip unless needed: action links (update, delete). Most clients already know the URL patterns from documentation.

A root endpoint for discovery

If you do implement any HATEOAS, consider adding a root endpoint that describes your API’s entry points:

route.get("/", {
  resolve: () => {
    return Response.json({
      name: "Bookstore API",
      version: "1.0.0",
      _links: {
        books: { href: "/books" },
        authors: { href: "/authors" },
        health: { href: "/health" },
      },
    });
  },
});

A developer who hits GET / immediately learns what resources are available and where to find them.

What’s next

That wraps up the individual patterns. In the next lesson, we’ll pull everything together into a design checklist you can use every time you build or review an API endpoint.

Exercises

Exercise 1: Add _links to the GET /books/:id response. Include self, author, reviews, update, and delete links.

Exercise 2: Add pagination links (next, self) to the GET /books response.

Exercise 3: Add a root endpoint (GET /) that lists all available resource URLs.

What is the main practical benefit of including links in paginated responses?

← Long-running operations API design checklist →

© 2026 hectoday. All rights reserved.