Skip to content

Commit 832b54b

Browse files
committed
Add the Implementers Guide
A build-it walkthrough structured on the §15 conformance classes: Server, Client, and BFF sections each ending in a conformance checklist. Covers choosing among the three §5.4 identifier forms, the opaque-identifier mis-resolution pitfall (with Go/JS standard-library gotchas), the §6.3 identifier-first template sources, the §10 trust chain with §13.2 matching notes, fail-small composition, and a client test-vector table built on the golang-vdp-demo endpoints. Linked from the nav (after Specification) and the home page.
1 parent 6e7d3af commit 832b54b

3 files changed

Lines changed: 370 additions & 0 deletions

File tree

docs/implementers-guide.md

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
# Implementers Guide
2+
3+
This guide walks through building a working VDP implementation. The
4+
[specification](specification.md) defines *what* conforms; this page is about
5+
*how* — the order to build things in, the decisions you will hit, and the
6+
mistakes that are easy to make.
7+
8+
The spec defines three [conformance classes](specification.md#15-conformance),
9+
and an implementation may belong to more than one:
10+
11+
| Class | Role | You are building… |
12+
|-------|------|-------------------|
13+
| [**VDP Server**](specification.md#151-vdp-server) | Produces view descriptors | an API that tells clients which templates render its responses |
14+
| [**VDP Client**](specification.md#152-vdp-client) | Consumes view descriptors | a renderer that resolves descriptors into template trees |
15+
| [**VDP BFF**](specification.md#153-vdp-bff) | A client that renders server-side | a backend-for-frontend returning finished markup |
16+
17+
The [Go demo](https://github.com/ViewDescriptorProtocol/golang-vdp-demo)
18+
implements all three in one process and is the reference to read alongside this
19+
guide — its `vdp` package is a complete client, its `server` package a complete
20+
server and BFF.
21+
22+
---
23+
24+
## Implementing a VDP Server
25+
26+
A server's job is small by design: attach a descriptor to each response. It
27+
never fetches templates and never renders.
28+
29+
### 1. Shape your descriptors
30+
31+
A view descriptor names a root template and, optionally, the sub-templates
32+
filling its named slots:
33+
34+
```json
35+
{
36+
"template": "example.com/templates/layouts/sidebar",
37+
"slots": {
38+
"mainContent": { "template": "example.com/templates/dashboard" },
39+
"sidebarNav": { "descriptor": "/views/nav.json" }
40+
}
41+
}
42+
```
43+
44+
Every descriptor you emit MUST validate against the
45+
[published schema](schema.md). Wire that into CI from day one — it is the
46+
cheapest conformance test you will ever write:
47+
48+
```bash
49+
npx ajv-cli test --spec=draft2020 \
50+
-s vdp.v0-1.schema.json -d 'views/*.json' --valid -c ajv-formats
51+
```
52+
53+
### 2. Choose an identifier form
54+
55+
A template URI is an **identity first** — a stable name and cache key — and a
56+
fetchable location only secondarily
57+
([Section 6.3](specification.md#63-template-sources)). The spec defines three
58+
forms ([Section 5.4](specification.md#54-url-resolution)):
59+
60+
| Form | Example | Behavior |
61+
|------|---------|----------|
62+
| **(a) Absolute URI** | `https://example.com/templates/card` | Identity as written |
63+
| **(b) Relative reference** — begins with `/` or `//` | `/templates/card` | Resolved against the transport's base URL |
64+
| **(c) Scheme-less opaque identifier** | `example.com/templates/card` | Identity as written; **never resolved against a base** |
65+
66+
Guidance:
67+
68+
- Use **absolute URIs** when descriptors may be consumed from multiple base URL
69+
contexts (the spec's own SHOULD).
70+
- Use **path-absolute references** when templates live on the same origin as
71+
the API — descriptors stay portable across your environments (dev, staging,
72+
production) because the base URL travels with the transport.
73+
- Use **opaque identifiers** when the URI is a *name* more than an address —
74+
package-import-style identities that clients look up in a bundle or registry,
75+
supplying a scheme only if they actually fetch.
76+
77+
Whatever you choose, remember that dot-relative values (`../templates/card`)
78+
fit **no** form: anything scheme-less that does not begin with `/` is an opaque
79+
identifier, so a conforming client will treat `..` as a host and reject it. Do
80+
not emit them.
81+
82+
### 3. Choose a transport
83+
84+
Any one of the [Section 4](specification.md#4-transport-mechanisms) transports
85+
satisfies conformance; which fits depends on how much you control the response
86+
body:
87+
88+
| Your response body is… | Use | Example |
89+
|------------------------|-----|---------|
90+
| Flexible JSON (HAL, custom) | Inline `_view` / `_views` | `{"_view": {...}, "revenue": 48200}` |
91+
| Rigid (OData4, third-party formats) | `Link` header to a standalone descriptor resource | `Link: <https://example.com/views/dashboard.json>; rel="view-descriptor"` |
92+
| Rendered by exactly one template | `View-Template` header shorthand | `View-Template: example.com/templates/login` |
93+
94+
Two transport rules trip people up:
95+
96+
- Emit **at most one** `Link` value with `rel="view-descriptor"` per response
97+
([Section 4.4](specification.md#44-precedence)); clients take the first.
98+
- When both an inline descriptor and a `Link` header are present, the body wins
99+
— so do not send both expecting the header to override.
100+
101+
### 4. Serve descriptor resources well
102+
103+
A standalone descriptor is an ordinary cacheable resource
104+
([Section 5](specification.md#5-view-descriptor-resources)). Serve it as
105+
`application/vdp+json` with real caching headers, and advertise the protocol
106+
version:
107+
108+
```http
109+
HTTP/1.1 200 OK
110+
Content-Type: application/vdp+json
111+
Cache-Control: public, max-age=3600
112+
ETag: "v1-dashboard"
113+
VDP-Version: 0.1
114+
```
115+
116+
Independent cacheability is the point: the descriptor changes when the
117+
*presentation* changes, the data endpoint when the *data* does.
118+
119+
### 5. Publish discovery
120+
121+
A discovery document at `/.well-known/vdp`
122+
([Section 13.2](specification.md#132-well-known-uri)) lets clients prefetch
123+
descriptors and learn your template allowlist:
124+
125+
```json
126+
{
127+
"version": "0.1",
128+
"endpoints": {
129+
"/api/dashboard": { "descriptor": "/views/dashboard.json" },
130+
"/api/products/{id}": { "descriptor": "/views/product-detail.json" }
131+
},
132+
"trustedTemplateUrls": [
133+
"https://example.com/templates/",
134+
"example.com/templates/"
135+
]
136+
}
137+
```
138+
139+
- Serve it as `application/vdp-discovery+json` — never as
140+
`application/vdp+json`.
141+
- End allowlist entries with a trailing slash, so `…/templates/` cannot match
142+
`…/templates-evil/`.
143+
- Allowlist matching never crosses identifier forms: an absolute entry matches
144+
only absolute URIs, a scheme-less entry only opaque identifiers. **List each
145+
form you actually emit** — the example above lists both.
146+
- Endpoint keys may be Level 1 URI Templates; each `{expression}` matches one
147+
path segment, and literal keys win over templated ones.
148+
149+
### 6. Add template metadata where it earns its keep
150+
151+
`type` is an advisory media-type hint; `integrity` is W3C Subresource
152+
Integrity for the template bytes
153+
([Section 3.6](specification.md#36-optional-template-metadata)). Publish
154+
`integrity` for templates hosted on infrastructure you don't control — it
155+
authenticates the *content* where the allowlist only authenticates the
156+
*origin*:
157+
158+
```json
159+
{
160+
"template": "https://cdn.example.net/templates/chart-legend",
161+
"type": "text/x-qute",
162+
"integrity": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
163+
}
164+
```
165+
166+
The digest is computed over the exact bytes clients will fetch —
167+
`base64(sha384(body))`.
168+
169+
### Server checklist
170+
171+
- [ ] Every descriptor validates against the published JSON Schema
172+
- [ ] At least one Section 4 transport, following its rules (one `Link` value max)
173+
- [ ] Descriptor resources served as `application/vdp+json` with caching headers
174+
- [ ] Absolute template URIs where descriptors cross base-URL contexts
175+
- [ ] Discovery document valid per Section 13.2, served as `application/vdp-discovery+json`
176+
- [ ] Allowlist entries end with `/` and cover every identifier form emitted
177+
178+
---
179+
180+
## Implementing a VDP Client
181+
182+
A client does the real work: extract, resolve, obtain, compose, render —
183+
the [Section 8 algorithm](specification.md#8-client-resolution-algorithm).
184+
Build it in this order.
185+
186+
### 1. Extract the descriptor
187+
188+
Check the response in [precedence order](specification.md#44-precedence):
189+
`_view` / `_views` in the body first, then the `Link` header
190+
(`rel="view-descriptor"`, first value), then `View-Template`. Record which
191+
transport won — it determines the **base URL** for relative references:
192+
193+
| Transport | Base URL for `/…` references |
194+
|-----------|------------------------------|
195+
| Standalone descriptor resource (`Link`) | the descriptor resource's own URL |
196+
| Inline `_view` / `_views` | the API response's URL |
197+
| `View-Template` header | the API response's URL |
198+
199+
A malformed descriptor is rejected outright
200+
([Section 9.3](specification.md#93-invalid-view-descriptor)) — fall back to
201+
raw data rather than guessing at partial meaning.
202+
203+
### 2. Classify every template URI
204+
205+
This is the step implementations get wrong. Classify each value **before**
206+
touching a URL library:
207+
208+
```text
209+
has a scheme → form (a): identity = the value, as written
210+
begins with "/" → form (b): identity = resolve(value, base)
211+
anything else → form (c): identity = the value, as written — opaque
212+
```
213+
214+
!!! warning "Never resolve an opaque identifier against the base URL"
215+
216+
General-purpose URL resolution treats `example.com/templates/card` as a
217+
*relative path*. Resolved against `https://api.example.org/dashboard`, it
218+
silently becomes `https://api.example.org/example.com/templates/card` — a
219+
corrupted identity that may even pass a sloppy allowlist before 404ing.
220+
The spec forbids this: a scheme-less value not beginning with `/` names
221+
the template directly and is compared and cached **verbatim**.
222+
223+
Watch your standard library, too: Go's `url.Parse` errors on
224+
`127.0.0.1:8080/templates/card` ("first path segment in URL cannot
225+
contain colon"), and JavaScript's `new URL(value, base)` happily
226+
mis-resolves it. Branch on the form first; parse afterwards.
227+
228+
The identity from this step — resolved absolute URL or verbatim opaque
229+
identifier — is the template's **cache key and comparison key** everywhere
230+
downstream. A scheme is supplied only if and when you fetch
231+
([Section 6.3](specification.md#63-template-sources)).
232+
233+
### 3. Obtain templates — from anywhere
234+
235+
The identifier tells you *which* template; your deployment decides *where its
236+
source text comes from*. All of these are equally conforming
237+
([Section 6.3](specification.md#63-template-sources), and see the
238+
[deployment scenarios](deployment-scenarios.md)):
239+
240+
- a bundle shipped inside the application package,
241+
- `<template>` elements delivered with the page,
242+
- a store local to the BFF or a template service,
243+
- a network fetch of the template URI itself.
244+
245+
Select by identity, whatever the source — a template satisfied locally is
246+
indistinguishable, to the rest of the algorithm, from a fetch whose cache was
247+
warm. Network fetch is the interoperable default when no local source has the
248+
template, and every network retrieval is subject to Section 10.
249+
250+
### 4. Enforce trust before any fetch
251+
252+
Rendering arbitrary templates is code injection. Before fetching, validate the
253+
*identity* against the [Section 10](specification.md#10-security-considerations)
254+
allowlist chain — first source available wins:
255+
256+
1. **Local configuration** — your own allowlist, when present.
257+
2. **Discovery document** — the API's `trustedTemplateUrls`.
258+
3. **Same-origin default** — only identities sharing the descriptor's origin.
259+
260+
Matching ([Section 13.2](specification.md#132-well-known-uri)) is prefix
261+
matching after normalization. Practical notes:
262+
263+
- Compare on path-segment boundaries, so a `…/templates` entry does not match
264+
`…/templates-evil`.
265+
- Lowercase the scheme and host before comparing; paths stay case-sensitive.
266+
- Forms never cross-match: opaque identifiers match only scheme-less entries,
267+
absolute URIs only absolute entries.
268+
- Reject *before* fetching. An untrusted template must never be
269+
fetched-then-discarded.
270+
271+
And the transport rule: network retrieval MUST use HTTPS, with plain HTTP
272+
acceptable only for loopback during development. Templates from inside your
273+
own trust boundary (a bundle, the page) are exempt from all of Section 10.
274+
275+
### 5. Verify integrity
276+
277+
When a descriptor carries `integrity`, verify any template you fetched over
278+
the network against it — W3C SRI semantics: strongest algorithm present wins,
279+
any one matching digest of that algorithm passes, unknown algorithms are
280+
ignored. A mismatch **is a fetch failure** for that slot
281+
([Section 9.1](specification.md#91-template-fetch-failures)), not a warning.
282+
283+
### 6. Compose, and fail small
284+
285+
Walk the descriptor recursively — obtain the root template, fill each slot,
286+
recurse ([Section 8](specification.md#8-client-resolution-algorithm)) — under
287+
the principle **prefer partial rendering over total failure**
288+
([Section 9](specification.md#9-error-handling)):
289+
290+
- A slot whose template cannot be obtained is **skipped**; the template's
291+
default content shows instead. The rest of the tree still renders.
292+
- A failed element of a slot *array* is skipped; the remaining elements render
293+
in declared order.
294+
- Only a **root** template failure fails the render — fall back to raw data or
295+
an error template.
296+
- Slot names with no matching insertion point are ignored (log them).
297+
- Impose a recursion depth limit (10 is the recommendation); descriptor
298+
references count toward it, and a reference chain that revisits a URL is a
299+
cycle that abandons just that slot.
300+
301+
Cache aggressively ([Section 5.2](specification.md#52-caching)): descriptors
302+
and templates are ordinary HTTP resources, keyed by identity.
303+
304+
### Client checklist
305+
306+
- [ ] Extraction follows Section 4.4 precedence
307+
- [ ] The three Section 5.4 identifier forms classified correctly — opaque ids never base-resolved
308+
- [ ] Templates selected and cached by identity, from any Section 6.3 source
309+
- [ ] Allowlist chain enforced before every network fetch; no cross-form matching
310+
- [ ] HTTPS enforced for network retrieval (loopback excepted)
311+
- [ ] `integrity` verified when present; mismatch treated as fetch failure
312+
- [ ] Partial rendering on slot failure; fallback on root failure; depth limit and cycle detection
313+
- [ ] Invalid descriptors rejected; unrecognized discovery members ignored
314+
315+
---
316+
317+
## Implementing a VDP BFF
318+
319+
A BFF is a VDP Client that runs server-side
320+
([Section 7.5](specification.md#75-bff-backend-for-frontend-pattern)): it
321+
meets **every** client requirement above in its role as consumer of upstream
322+
APIs, then returns finished markup. VDP places no constraints on the interface
323+
it exposes downstream — the browser never sees a descriptor.
324+
325+
What changes in practice:
326+
327+
- **Negotiate for your platform.** Send `VDP-Platform` (and standard content
328+
negotiation) on descriptor fetches so the API can select the right template
329+
tree ([Section 5.5](specification.md#55-client-specific-selection)).
330+
- **Cache across users.** Descriptors and templates are shared, cacheable
331+
state; per their HTTP headers, one warm cache serves every request.
332+
- **Keep failure behavior.** Section 9's partial rendering applies to the page
333+
you assemble: a dead slot ships as the template's default, not as a 500.
334+
335+
---
336+
337+
## Testing your implementation
338+
339+
**Servers:** schema-validate every descriptor and discovery document you emit
340+
(the [ajv commands](schema.md#validation) mirror the spec repo's CI), then
341+
check the transport rules — exactly one `Link` value, correct media types,
342+
`VDP-Version` where you advertise support.
343+
344+
**Clients:** the hard cases are the failure paths and the identifier forms.
345+
The [Go demo](https://github.com/ViewDescriptorProtocol/golang-vdp-demo)
346+
serves ready-made vectors — run it locally and point your client at:
347+
348+
| Endpoint | Exercises | Your client should |
349+
|----------|-----------|--------------------|
350+
| `/api/dashboard` | Link transport, form (b) refs, §3.7 reference, integrity | Render a four-level tree |
351+
| `/api/dashboard?fail=chart` | One slot's template 404s | Skip the slot, render the rest |
352+
| `/api/dashboard?fail=root` | Root template 404s | Fall back to raw data |
353+
| `/api/dashboard?fail=integrity` | SRI mismatch | Treat as fetch failure, skip the slot |
354+
| `/api/dashboard?untrusted` | Off-allowlist template | Reject **without fetching** |
355+
| `/api/odata/products` | Link + OData annotation, form (c) opaque identifier | Keep the identifier verbatim as cache key |
356+
| `/api/login` | `View-Template` shorthand | Render the single template |
357+
| `/api/products/42?view=compact` | Multiple named views | Select the requested view, default otherwise |
358+
359+
And remember what is deliberately **not** VDP's job — do not build it into
360+
your implementation: conditional slot logic, template parameters, and
361+
data-to-template field mapping all belong to the server's descriptor choice or
362+
the template engine, never to the protocol
363+
([Design Decisions](specification.md#design-decisions)).
364+
365+
*[VDP]: View Descriptor Protocol
366+
*[HAL]: Hypertext Application Language
367+
*[BFF]: Backend for Frontend
368+
*[SRI]: Subresource Integrity

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ A VDP view descriptor tells the client to render a sidebar layout, filling its s
117117
VDP is an **early working draft** (v0.1, alpha). The specification is feature-complete for v0.1 and the [Go demo](https://github.com/ViewDescriptorProtocol/golang-vdp-demo) implements it end to end; feedback and implementations are welcome.
118118

119119
<div class="vdp-links" markdown>
120+
[Implementers Guide](implementers-guide.md){ .secondary }
120121
[GitHub Organization](https://github.com/ViewDescriptorProtocol){ .secondary }
121122
[JSON Schema](schema.md){ .secondary }
122123
</div>

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extra_css:
4545
nav:
4646
- Home: index.md
4747
- Specification: specification.md
48+
- Implementers Guide: implementers-guide.md
4849
- Schema: schema.md
4950
- Examples: examples.md
5051
- Deployment Scenarios: deployment-scenarios.md

0 commit comments

Comments
 (0)