Scripted Responses
Most mocks return a static body or a template with Faker/JsonPath placeholders. When a
response needs real logic — a calculation, a branch on the request, state that changes
between calls, or deliberate flakiness for resilience testing — use a scripted response.
A scripted response is JavaScript that runs every time the mock is hit. It receives the
incoming request and fills an outgoing response. It is just another response variant,
alongside Body, Empty, and File Template.
Scripted responses are available for HTTP, gRPC, GraphQL, SOAP, MCP, Kafka,
RabbitMQ, NATS, and Socket mocks. For the non-HTTP protocols the request is adapted to the same
requestobject and the producedresponseis mapped back to that protocol’s
reply (see Other protocols below).
Create a scripted response in the UI
- Open the Constructor and define the route and method as usual.
- Under Response Body Type, click Script.
- Write your script in the editor. Autocomplete (Ctrl+Space, or type
request.) suggests
everything available. - Type an example request body in Try it and press Run example to see the response
the script produces, plus anyconsole.logoutput — without saving. - Save the mock.
The request object (input)
Read-only. The incoming request, broken into parts:
| Field | Description |
|---|---|
request.method |
HTTP method, e.g. "POST" |
request.path |
Request path the mock matched, e.g. "/api/orders/42" |
request.route |
The mock’s route pattern, e.g. "/api/orders/:id" |
request.url |
Full URL |
request.host |
Request host |
request.remoteAddr |
Client address |
request.query |
Query parameters (first value), e.g. request.query.page |
request.queryAll |
Query parameters as arrays (all values), e.g. request.queryAll.tag |
request.queryString |
Raw query string, e.g. "page=2&tag=a" |
request.params |
Path parameters from the route pattern, e.g. request.params.id for /api/orders/:id |
request.headers |
Headers, first value, lower-cased keys, e.g. request.headers["content-type"] |
request.headersAll |
Headers as arrays (all values), lower-cased keys |
request.header(name) |
Case-insensitive single header lookup |
request.cookies |
Cookies by name |
request.body |
Raw request body as a string |
request.json() |
Parses the body as JSON and returns the object |
request.protocol |
The protocol the mock answers, e.g. "http", "grpc", "kafka" |
request.attrs |
Protocol-specific fields for non-HTTP mocks (e.g. the gRPC service/method, the Kafka topic, the MCP tool name). The common fields above always win on a key clash. |
Every field of the incoming request is passed to the script — nothing is dropped.
The response object (output)
Mutable. Its final state becomes the response Mockarty sends:
| Field / method | Description |
|---|---|
response.status(201) |
Set the HTTP status code (default 200). response.status = 201 also works. |
response.json(obj) |
Set the body to JSON and Content-Type: application/json |
response.text(s) |
Set a plain-text body |
response.body = "..." |
Set the body directly |
response.setHeader(name, value) |
Add a response header (alias: response.header(name, value)) |
response.delay(200) |
Delay the response by N milliseconds. response.delay = 200 also works. |
The method forms are chainable, so you can write response.status(201).setHeader("X-Run", "1").json({ ok: true }).
You can also return a value as a shortcut: if you don’t set response.body, a returned
object becomes the JSON body.
Form values seed the response. The Status Code and Response Headers you set on the mock’s
form are pre-loaded as the script’s starting response — so a script that never touches the
status still returns the form’s status, and the form headers are already present. Anything the
script sets wins: response.status(503) overrides a form status of 201. This also drives the
Run example preview, so the dry run shows exactly what the live mock would return.
The mock object (metadata)
Read-only information about the mock that is currently responding:
| Field | Description |
|---|---|
mock.id |
The mock’s identifier |
mock.namespace |
The workspace the mock belongs to |
mock.route |
The mock’s route, e.g. /api/orders/:id |
mock.protocol |
http, grpc, graphql, soap, mcp, kafka, rabbitmq, nats, or socket |
mock.chainId |
The chain this mock is part of (empty if none) — use it to key store.chain for a clean per-flow scope |
// Tag the response with where it came from.
response.setHeader("X-Mock-Id", mock.id);
response.json({ servedBy: mock.id, chain: mock.chainId });
Stores and environment
Scripts read and write the same Mock, Chain, and Global stores as the rest of Mockarty,
so a script can keep state across requests:
// store.mock — scratch for this single request
// store.chain — shared across mocks in the same chain (an order flow)
// store.global — shared across the whole workspace
const visits = (store.global.get("visits") || 0) + 1;
store.global.set("visits", visits);
response.json({ visits: visits });
Each scope offers get(key), set(key, value), has(key), delete(key), and keys() —
you can both read and write. Chain and Global writes persist through the shared cache, so a
value set by one mock is visible to the next mock in the chain (and across nodes). Mock scope
is scratch for the current request.
env.get("KEY") reads workspace environment values.
Secrets are read-only — a script can read a secret to use it (e.g. inject an API key into
an outbound call) but never write one:
const apiKey = store.secrets.get("vault", "api_key");
if (store.secrets.has("vault", "api_key")) { /* ... */ }
Secrets are scoped to the mock’s workspace and are masked in the “Run example” preview (you
see a placeholder, never the real value). store.secrets is served by the main Mockarty
node; a mock exported to a standalone generated server runs without the secrets store, so
read any secret you need into a header or the body on the main node instead of relying on it
downstream.
The mk helpers
| Helper | Example |
|---|---|
mk.faker.* |
A focused set of generators: name, firstName, lastName, email, username, phone, uuid, word, sentence, paragraph, url, ipv4, ipv6, date, timestamp, boolean, macAddress, currency, creditCard, domainName. (The larger $.fake.* catalogue documented in the Faker reference is for template responses, not scripts.) |
mk.uuid() |
Random UUID |
mk.randomInt(min, max) |
Random integer |
mk.crypto.* |
mk.crypto.sha256(s), mk.crypto.hmac("sha256", key, msg) |
mk.base64.* |
mk.base64.encode(s), mk.base64.decode(s) |
mk.jsonpath(obj, path) |
Extract a value by JsonPath |
mk.http.send(opts) |
Outbound HTTP request — off by default (see below) |
Examples
Compute from the request:
const data = request.json();
response.json({
orderId: request.params.id,
total: data.amount * 1.1,
currency: "USD"
});
Branch on a header:
if (request.header("authorization")) {
response.json({ user: "alice@example.com" });
} else {
response.status = 401;
response.json({ error: "unauthorized" });
}
Stateful counter (chain store):
const n = (store.chain.get("hits") || 0) + 1;
store.chain.set("hits", n);
response.json({ count: n });
Toxicity: faults and latency
Scripts are the simplest way to make a mock behave like an unreliable upstream — useful for
resilience and chaos testing:
// Fail 10% of requests, add 200ms latency to the rest
if (Math.random() < 0.1) {
response.status = 503;
response.json({ error: "service unavailable" });
return;
}
response.delay = 200;
response.json({ ok: true });
Outbound calls
By default a script cannot reach the network — mk.http.send is disabled. Enable Allow
outbound calls on the script (or scriptAllowNet in the API) only when the response must
fetch from an external system. Outbound targets are validated to prevent requests to
private, loopback, or cloud-metadata addresses.
// requires "Allow outbound calls" to be enabled
const upstream = mk.http.send({ method: "GET", url: env.get("UPSTREAM_URL") });
response.status = upstream.status;
response.body = upstream.body;
For heavier side effects (publishing to a queue, notifying another service after responding),
prefer Webhooks & Callbacks, which run asynchronously after the
response is sent.
Limits
Scripts run with a tight time budget (50 ms by default; configurable per mock up to 1
second). A script that exceeds it is stopped and the mock returns 500. Use the budget for
fast logic; offload slow work to callbacks.
A script’s source is limited to 256 KB. When the script sets an HTTP status outside the
valid 100–999 range, the mock falls back to 200.
Validation and errors
The editor checks your script as you type: a green Valid badge means it compiles, and a
red badge shows the first syntax error with its line number. A mock with a script that does
not compile is rejected when you save it, with a message pointing at the line — so a broken
script never ships silently.
At run time, if the script throws (or its body is invalid JSON you read with
request.json()), the mock responds with a clear JSON error instead of a blank 500:
{ "error": "mock script error", "detail": "TypeError: ... at line 3" }
request.json() returns null when the request has no body, so the common guard
const body = request.json() || {} works without a special case.
Create via API
A scripted response is the script field on the response:
curl -X POST "http://localhost:5770/api/v1/mocks?namespace=sandbox" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "calc",
"http": { "route": "/api/calc/:op", "httpMethod": "POST" },
"response": {
"script": {
"code": "const d=request.json(); response.json({op:request.params.op, out: request.params.op===\"double\"? d.v*2 : d.v/2});",
"allowNet": false
}
}
}'
Test a script against an example request without saving:
curl -X POST "http://localhost:5770/api/v1/mocks/script/preview?namespace=sandbox" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "script": "response.json({ hi: request.json().name });", "request": { "body": "{\"name\":\"world\"}" } }'
Other protocols
Scripted responses also work for gRPC, GraphQL, SOAP, MCP, Kafka, RabbitMQ,
NATS, and Socket mocks. The request and response objects adapt to each protocol — the
protocol’s fields appear directly on request (and the full set on
request.attrs):
| Protocol | On request |
Build the response with |
|---|---|---|
| gRPC | request.service, request.method, request.message (also request.json()) |
response.json(obj) — the reply message; response.status = gRPC code |
| GraphQL | request.operation, request.field, request.variables, request.queryText |
response.json({ data: {...} }) |
| SOAP | request.action, request.method, request.service, request.bodyData |
response.text("<...>") — the XML body |
| MCP | request.tool, request.arguments (also request.json()) |
response.json({ content: [{ type: "text", text: "..." }] }) |
| Kafka | request.topic, request.value (also request.json()) |
response.json(obj) — the reply payload |
| RabbitMQ | request.queue, request.value (also request.json()) |
response.json(obj) |
| NATS | request.subject, request.reply, request.queueGroup, request.message (also request.json()) |
response.json(obj) — the reply payload |
| Socket | request.serverName, request.event, request.message (also request.json()) |
response.json(obj) |
Headers work everywhere. request.headers and request.header(name) behave
the same as for HTTP across protocols:
- gRPC — call metadata is exposed as headers, so
request.header("authorization")
reads a metadata entry (the raw map is also atrequest.metadata).request.route
is the canonicalservice/method. - GraphQL, SOAP, MCP — these arrive over HTTP, so the client’s request headers are
available viarequest.header(name)/request.headers, andrequest.path/
request.routehold the matched endpoint path. - Kafka, RabbitMQ, NATS — message headers are exposed via
request.headers.
Stores, env, secrets, mk.*, and the budget all work the same way as for HTTP.
// gRPC mock script
const u = request.message; // the request message
response.json({ id: u.id, name: "User " + u.id });