Recipes

List and Detail From Data

The most common pair of endpoints in any API: a collection and a detail view of the same resources. This recipe serves both from one data block, so the fixtures live in a single place and unknown ids return a proper 404.

The recipe

data "products" {
product {
id = 1
content = <<-EOF
{
"id": 1,
"name": "Widget",
"price": 9.99
}
EOF
}
product {
id = 2
content = <<-EOF
{
"id": 2,
"name": "Gadget",
"price": 24.99
}
EOF
}
product {
id = 3
content = <<-EOF
{
"id": 3,
"name": "Doohickey",
"price": 4.99
}
EOF
}
}

mock "GET /products" {
format = "json"
body = <<-EOF
[
{{#forEach data.products.product}}
{{item.content}}{{^isLast}},{{/isLast}}
{{/forEach}}
]
EOF
}

mock "GET /products/{id}" {
format = "json"
body = <<-EOF
{{= $id request.params.id}}
{{= $found false}}
{{#forEach data.products.product}}
{{#is item.id $id}}
{{= $found true}}
{{item.content}}
{{/is}}
{{/forEach}}
{{#unless $found}}
{{setStatus 404}}
{ "error": "Not found" }
{{/unless}}
EOF
}

Try it

$curl http://localhost:8080/products/2
{ "id": 2, "name": "Gadget", "price": 24.99 }
$curl -i http://localhost:8080/products/99
HTTP/1.1 404 Not Found ... { "error": "Not found" }

How it works

  • Each product carries an id for lookups and a content heredoc holding the full payload. The list endpoint just concatenates the payloads with forEach and {{^isLast}},{{/isLast}} commas.
  • The detail endpoint extracts the path parameter to $id before the loop, because inside forEach the context changes and request is out of scope.
  • is compares with loose equality, which matters here: the path parameter is the string "2" while the data block id is the number 2. eq would never match.
  • $found starts false and flips when a product matches. Variables have global scope, so the assignment inside the loop is visible to the unless block after it, which turns no-match into a 404.
This pattern is the read-only sibling of Stateful CRUD. The two combine well: serve the baseline catalog from data and layer runtime changes on top with flags.

Variations

  • For paginated lists, extract data.products.product to a variable and combine slice, length, and query parameters. The helpers are in the Bigodon reference.
  • To filter the list by a query parameter, reuse the detail endpoint's loop-and-match shape, accumulating matches instead of rendering a single one.