Explain the Affordances API in Spring HATEOAS: what `afford(...)` does and how affordances relate to HAL-FORMS.
answer
- afford(methodOn(...).update(null,id))
- link.andAffordance(...)
- renders only in HAL-FORMS _templates
- plain HAL drops affordances
- derives verb + @RequestBody props + validation
basics
~10 sAn affordance describes an action you can take on a resource (e.g. update, delete) attached to a link. You add one with afford(methodOn(controller).method(...)). Affordances render as HAL-FORMS _templates; plain HAL ignores them.
solid answer
~40 sThe Affordances API lets a link carry not just where a resource is but what you can *do* to it. You attach affordances to a link via the `afford` helper: `linkTo(methodOn(Ctrl.class).one(id)).withSelfRel().andAffordance(afford(methodOn(Ctrl.class).update(null, id)))`. Each affordance is derived by inspecting the target controller method: Spring reads its HTTP verb (`@PutMapping` → PUT), its `@RequestBody` input type to build the list of form properties, and Bean Validation annotations on that type (`@NotNull`, `@Pattern`) to mark properties required or add regex. Crucially, affordances only surface when the response is rendered as **HAL-FORMS** (`application/prs.hal-forms+json`), where they become the `_templates` block describing each writable operation's method, contentType, and properties. In plain HAL (`application/hal+json`) affordances are silently dropped. So affordances make an API self-describing for writes, letting a client build a form to update/delete without out-of-band docs.
code
java · 17 linesimport static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.EntityModel;
EntityModel<Employee> toModel(Employee e) {
var self = linkTo(methodOn(EmployeeController.class).one(e.getId())).withSelfRel()
// PUT affordance: properties come from UpdateEmployeeRequest + its validation
.andAffordance(afford(methodOn(EmployeeController.class)
.update(null, e.getId())))
// DELETE affordance: no body properties
.andAffordance(afford(methodOn(EmployeeController.class)
.delete(e.getId())));
return EntityModel.of(e, self);
}
// Rendered ONLY under Accept: application/prs.hal-forms+json as:
// "_templates": { "default": { "method": "put", "properties": [...] },
// "delete": { "method": "delete" } }go deeper
Vaguely knows affordances describe possible actions on a resource.
Can call afford(methodOn(...)) and attach it via andAffordance, and knows it maps to HAL-FORMS.
Explains the media-type gate, property/validation inference, and placing affordances in the assembler.
Weighs affordances as a machine-readable write contract vs. OpenAPI, and reasons about client-generated forms and API evolution.
**Motivation.** Base HAL only answers *where can I go* (GET links). It says nothing about *how to change state* — the HTTP method, the request body shape, or which fields are required. The **Affordances API** fills that gap. An *affordance* is a description of an operation available on a resource, attached to an existing link. **Creating affordances.** The entry point is the static helper `afford(...)`, imported from `org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.afford` (or `Affordances.of(link).afford(...)` for a fluent style). You typically pass a fake controller invocation: ```java afford(methodOn(EmployeeController.class).updateEmployee(null, id)) ``` and attach it with `link.andAffordance(...)`. A single link can carry several affordances (update *and* delete). **What Spring derives from the target method.** When building the affordance, Spring HATEOAS inspects the referenced handler method: - **HTTP method** — from `@PutMapping`/`@PostMapping`/`@DeleteMapping` etc. A DELETE affordance has no body properties; a PUT/POST does. - **Input properties** — from the method's `@RequestBody` parameter type. Each bean property becomes a HAL-FORMS *property* entry (name, and type where inferable). - **Validation metadata** — Bean Validation (JSR-380) annotations on the input type drive HAL-FORMS attributes: `@NotNull`/`@NotBlank` → `required: true`; `@Pattern` → `regex`; size constraints can map to `maxLength`. This means your form contract stays in sync with your validation rules automatically. - **Affordance name** — defaults to the method name; you can rename via `.withName(...)` on the `Affordance`/`Affordances` builder, which becomes the `_templates` key. **Rendering — the media-type gate (the key gotcha).** Affordances are *only* materialized when the response is negotiated as **HAL-FORMS**, media type `application/prs.hal-forms+json` (`MediaTypes.HAL_FORMS_JSON`). There they appear as a `_templates` object alongside `_links`: ```json "_templates": { "default": { "method": "put", "contentType": "application/json", "properties": [ { "name": "name", "required": true }, { "name": "role" } ] } } ``` In plain **HAL** (`application/hal+json`) the affordances are simply ignored — you get only `_links`. So adding affordances is harmless to HAL clients but requires HAL-FORMS support to be useful. Enable HAL-FORMS with the Boot starter (it's negotiated automatically) or `@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)`. **Auto-collected affordances.** When you build a self link and there are other handler methods mapped to the *same* URI, some configurations surface those as affordances automatically, but the reliable, explicit approach is `andAffordance(afford(...))` in your assembler. **Where to put them.** The idiomatic home is the `RepresentationModelAssembler.toModel`, so every representation of the entity advertises the same write operations consistently. **Gotchas.** - Passing `null` for the body argument in `methodOn(...).update(null, id)` is normal — only the *type* matters, not the value. - If your client never sends `Accept: application/prs.hal-forms+json`, it will never see `_templates`; teams sometimes debug 'missing affordances' that are actually a content-negotiation issue. - Affordance property inference depends on a concrete `@RequestBody` type; a `Map` or generic body yields empty/opaque properties. - Names collide if two affordances resolve to the same template key; rename explicitly. **When to use.** HAL-FORMS + affordances shine when you want truly self-describing, evolvable write APIs (clients render forms dynamically). If clients are hand-coded and you control both ends, the added machinery may not be worth it.
- A candidate adds affordances but the response has no `_templates`. What's the most likely cause?The client requested plain HAL (`application/hal+json`) or `application/json`; affordances only render under HAL-FORMS (`application/prs.hal-forms+json`). It's a content-negotiation issue, not a code bug.
- Where do the properties and `required` flags in a `_templates` entry come from?From the afforded method's `@RequestBody` type: each bean property becomes a form property, and Bean Validation annotations like `@NotNull`/`@Pattern` set `required`/`regex`.
saying these in an interview costs you the question
- Believing affordances render in plain HAL (`application/hal+json`).
- Thinking the value passed to `methodOn(...).update(null, id)` matters — only the type is used.
- Confusing affordances (`_templates`) with base HAL `_links`.
- Assuming affordances need a special annotation rather than `afford(...)` on a link.