Echolot
Mocks

Scripted routes

Answer a mock's requests in JavaScript before its other routes and its spec, with state kept while the mock runs.

Routes made by hand and a spec answer with responses you write down. Sometimes a mock needs to do more: remember the order a client just placed, compute an answer, or fail on the third call. Routes in JavaScript are small handlers that answer a mock's requests first. Their variables live as long as the mock runs, so they can keep state between requests. Anything a route doesn't handle goes on to the mock's routes made by hand, then to its spec.

Writing routes

Open a mock on the Mocks page and scroll to the Routes in JavaScript card. Write the routes in the editor, then click Save Routes. Revert goes back to what was saved.

The Routes in JavaScript card of a mock with code that keeps orders in a list, the line Running: POST /orders, GET /orders/:id below the editor, and the log naming the script route that answered each order request

This example, for a mock of the Alpenglow spec, keeps the orders a client places and returns them by ID:

let orders = []

mock.post("/orders", (req, res) => {
  const order = { id: "ord_" + (orders.length + 1), status: "pending", ...req.json() }
  orders.push(order)
  res.status(201).header("Location", "/v1/orders/" + order.id).json(order)
})

mock.get("/orders/:id", (req, res) => {
  const order = orders.find(o => o.id === req.params.id)
  return order ?? res.status(404).json({ error: "no such order" })
})

After a save, the card lists the routes the script registered (Running: POST /orders, GET /orders/:id), or, in orange, why the script didn't load. The routes are kept in mocks/<name>.js beside the mock's settings, so they go into Git with the workspace.

When routes run

  • Routes in JavaScript answer first, before routes made by hand and the spec. The first route whose method and path match answers.
  • Paths are below the spec's server path, when the mock serves a spec: write /orders, not /v1/orders.
  • A route that sets nothing and returns nothing passes the request on to the routes made by hand and the spec, which answer as usual. Use this to handle only some requests, such as one query parameter.
  • The script runs once when the mock starts. Its top-level variables, and the state object, live until the mock stops.
  • Saving restarts the routes, with fresh state. Changes to the spec, the routes made by hand or the mock's settings keep it.
  • Requests are handled one at a time, so state stays consistent.

The mock's log marks each request a route handled with the route's name: rest the pointer on it for Answered by this route or Passed on by this route (to the routes made by hand or the spec). What a route writes with console.log appears in the log under the request.

The routes API

Register routes with mock and a method:

FunctionMatches
mock.get(path, handler)GET
mock.post, mock.put, mock.patch, mock.delete, mock.head, mock.optionsThat method
mock.any(path, handler)Every method

In a path, :name matches one segment and puts it in req.params.name; * matches the rest.

A handler gets req and res:

reqWhat it holds
req.methodThe method, in capitals.
req.pathThe path, below the spec's server path.
req.paramsThe path's :name segments.
req.queryThe query parameters, the last value of each name.
req.headersThe headers, with lower-case names.
req.bodyThe body as text.
req.json()The body parsed as JSON, or null when there is none.
resWhat it does
res.status(code)Sets the status. The default is 200.
res.header(name, value)Adds a header.
res.json(value)Sends the value as JSON, with Content-Type: application/json unless you set one.
res.send(text)Sends text, with Content-Type: text/plain; charset=utf-8 unless you set one.

Each res function returns res, so calls chain: res.status(201).json(order). A value the handler returns is sent as JSON, so return order is the same as res.json(order). Handlers may be async.

state (also mock.state) is an object kept between requests, for what you'd rather not keep in a variable of your own:

mock.get("/me", (req, res) => {
  state.calls = (state.calls ?? 0) + 1
  if (state.calls % 3 === 0) return res.status(503).json({ error: "try again" })
  // Nothing set: the routes made by hand or the spec answer this request.
})

Limits

Routes run in the same sandbox as request scripts, not in Node: no file system and no network. Each request may run JavaScript for 2 seconds, and the script has 2 seconds to start. A handler that throws answers 500 with the error, which also shows in the log.

Trusting routes

Routes are code that runs on your Mac, so they follow the same rule as request scripts. Routes you write and save in Echolot run. Routes that came from outside, through Git, an import or another app editing the file, don't: the card says These scripts came from outside Echolot, and the mock doesn't start until you read them and click Trust Scripts. If they change outside Echolot again, Echolot asks again.

Agents can read a mock's routes but never write them.

From the command line

echolot mock -w <workspace> <mock> runs a workspace's mock with all its routes; --routes <file> runs routes in JavaScript from another file, also with a spec file. See echolot mock.

On this page