Creating Mocks
Templating
Mock bodies are templates written in Bigodon, a Handlebars-like language. By the end of this page you will be able to render request data into responses, branch on conditions, override the status code, and build JSON arrays with loops. We will grow one orders API from a static response to a fully dynamic one.
Your first template
Anything between {{ and }} is a template expression, evaluated on every request. The simplest expression is a path into the request:
mock "GET /orders/{id}" {
format = "json"
body = <<-EOF
{
"id": {{request.params.id}},
"status": "shipped"
}
EOF
}The request context
Every part of the incoming request is available under request:
{{request.params.id}}reads path parameters declared with{id}in the route.{{request.query.page}}reads query string values,?page=2in this case.{{request.headers.x-user-id}}reads request headers. Header names are always lowercase here, no matter how the client sent them.{{request.body.email}}reads fields from a parsed JSON or form body.
Paths traverse nested objects with dots, so {{request.body.customer.email}} works exactly as you would expect.
Helpers
Helpers are functions called inside an expression. The first word is the helper name and the rest are arguments, which can be context paths, quoted literals, or numbers:
{{capitalize request.params.name}}
{{default request.query.page 1}}
{{add request.query.page 1}}defaultreturns its first argument unless it is missing, then falls back to the second. To pass one helper's result to another, wrap the inner call in parentheses:
{{capitalize (default request.query.name 'stranger')}}Bigodon ships helpers for strings, math, arrays, dates, and comparisons, and Mocko adds its own on top. This page introduces the ones you will use constantly; the Bigodon reference and template helpers reference list them all.
Conditionals
A block runs its content only when its expression is truthy. Blocks open with {{#...}} and close with {{/...}}, and can have an {{else}}. The is helper compares two values with loose equality, which is usually what you want because path parameters and query values arrive as strings:
mock "GET /orders/{id}" {
format = "json"
body = <<-EOF
{{#is request.params.id 1}}
{ "id": 1, "status": "shipped" }
{{else}}
{ "id": {{request.params.id}}, "status": "processing" }
{{/is}}
EOF
}Chain more cases with {{else is ...}}, closing only the outermost block:
{{#is request.params.id 1}}
{ "status": "shipped" }
{{else is request.params.id 2}}
{ "status": "cancelled" }
{{else}}
{ "status": "processing" }
{{/is}}Other comparison helpers work the same way in block position: eq (strict equality), gt, gte, lt, lte, startsWith, includes, plus if and unless for plain truthiness.
Overriding the status code
The status field is static, but templates can override it per request with setStatus. It renders as an empty string, so it never pollutes the body. This is the standard way to mock error branches:
mock "GET /orders/{id}" {
format = "json"
body = <<-EOF
{{#gt (toInt request.params.id) 100}}
{{setStatus 404}}
{ "error": "Order not found" }
{{else}}
{ "id": {{request.params.id}}, "status": "processing" }
{{/gt}}
EOF
}{{setHeader 'name' 'value'}}does the same for response headers: side effect only, empty output, and it merges with the mock's headers block.
toInt converts the path parameter to a number before the comparison. Params and query values are strings, and while loose helpers like is coerce for you, ordering comparisons like gt are safest on real numbers.Variables
{{= $name value}} assigns a variable, and {{$name}} reads it. Variables keep templates readable when a value is used more than once, and they become essential inside loops, as you will see next:
body = <<-EOF
{{= $page (toInt (default request.query.page 1))}}
{
"page": {{$page}},
"next": {{add $page 1}}
}
EOFVariables are per request. Each incoming request starts from a clean slate, so they are the right tool for intermediate values. For state that survives across requests, Mocko has flags.
Loops
forEach iterates an array. Inside the block the context changes: item is the current element, and you also get index, total, isFirst, and isLast. The negated block {{^isLast}},{{/isLast}} renders a comma after every element except the last one, which is exactly what JSON arrays need:
mock "POST /orders" {
format = "json"
body = <<-EOF
{
"status": "created",
"items": [
{{#forEach request.body.items}}
{
"sku": "{{item.sku}}",
"position": {{index}}
}{{^isLast}},{{/isLast}}
{{/forEach}}
]
}
EOF
}each helper, but isLast and friends do not exist inside it, and a {{^isLast}} block there silently renders on every iteration, producing a trailing comma and invalid JSON. When building JSON arrays, always use forEach.When a value comes back empty
Inside forEach the context is the loop entry, so request and other top-level paths are no longer directly reachable. A template that reads {{request.query.currency}} inside a loop renders an empty string, which is one of the most common template bugs. There are two fixes:
{{! 1. Extract to a variable before the loop (variables stay accessible) }}
{{= $currency request.query.currency}}
{{#forEach request.body.items}}
{ "sku": "{{item.sku}}", "currency": "{{$currency}}" }{{^isLast}},{{/isLast}}
{{/forEach}}
{{! 2. Or reach back to the template root with $root }}
{{#forEach request.body.items}}
{ "sku": "{{item.sku}}", "currency": "{{$root.request.query.currency}}" }{{^isLast}},{{/isLast}}
{{/forEach}}The same applies to any block that changes context, such as each and with. $parent steps one context level up and can be chained; $root always jumps to the top.
Debugging templates
Three signals cover almost every template problem:
- A template that fails to compile is reported in the terminal when the mock loads, with a line and column pointer.
- If a JSON response comes back as raw, unformatted text, the rendered body was not valid JSON. Mocko logs an error saying so. The usual suspects are a trailing comma from
each, an empty value from a context mistake, or missing quotes around a string. {{log 'reached the else branch'}}prints to the server console. Combine it with thejsonhelper to dump a value:{{log (json request.body)}}.
Behavior details
- Variables have global scope within a request: an assignment inside a block persists after the block closes. This enables accumulator patterns, but also means loops do not sandbox your variables.
defaultonly falls back on missing values. Empty strings and0pass through, so?page=yields an empty string, not your fallback. Useunlessor a length check for blank-string semantics.- Literal braces in a body must be escaped as
\{{and\}}. Inside HCL quoted strings (not heredocs), write\\{{because HCL consumes one backslash itself. - Block negation
{{^expr}}and blocks in general work on helper calls and context paths, not on variables. For a variable, use{{#if $var}}or{{#unless $var}}. - Date helpers need ISO strings with an explicit time part:
2024-01-01T00:00:00.000Zworks,2024-01-01does not. {{random 0 100}}is inclusive on both ends.
Next
With several mocks defined, which one answers a given request? Continue to How Matching Works.