Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mock APIs (OpenAPI)

Upload an OpenAPI document and Rustybin serves it as a named mock API at /mock/<name>: every operation answers with a response that matches the spec, every call is captured, and security from the spec (API keys, basic, bearer, OAuth 2.0 scopes, mTLS) can be switched on in front of it. Point a gateway at the mock URL and you have a realistic upstream for an API that does not exist yet, or that you cannot reach from a demo laptop.

  • OpenAPI 3.0 and 3.1, as JSON or YAML, one bundled file (local #/... references only; external $refs and Swagger 2.0 are refused with a clear message).
  • Responses come from the spec's examples or are generated from the schemas, deterministically, with path parameters echoed (GET /pets/42 returns "id": 42). Rules, overrides and the Prefer header pick other documented responses. See Responses, rules and validation.
  • Requests are validated against the spec (warn by default, or enforce with 400).
  • Each mock has a draft and a live revision: edits are checked (a valid request and response per operation) before they are deployed, and a deploy can be rolled back.
  • Mocks are created in the web console, through the control API, from a rustybin.yaml file or a directory of specs at startup (Configuration as code), or with the rustybin mock CLI.

Mocks live in memory. Mocks declared in files are recreated at every start; mocks created in the console or through the API are lost on restart unless you export them to a rustybin.yaml (Export and apply).

Quick start

In the console

  1. Open /ui/#/mocks (for example http://localhost:8080/ui/#/mocks).
  2. Drop an OpenAPI file on the page, or paste it. Rustybin parses it at once and shows the operations, the security schemes and any warnings, with a name derived from info.title.
  3. Choose how responses are produced (spec examples, generated data, or your own), whether to secure the mock and whether to validate requests, then Create and deploy.
  4. Copy the mock URL (or the ready curl command) into your gateway. The mock's Traffic tab shows each call as it arrives.

With the API

POST /_rustybin/mocks takes the spec (a JSON object, or the document text) and optional settings, runs the deploy checks and makes the mock live (201). The admin token is needed when the instance has one (the example server does):

POST {{base_url}}/_rustybin/mocks
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
  "name": "docs-orders",
  "spec": {
    "openapi": "3.0.3",
    "info": {"title": "Orders API", "version": "1.0"},
    "servers": [{"url": "https://orders.example.com/api"}],
    "paths": {
      "/orders": {
        "get": {
          "operationId": "listOrders",
          "responses": {"200": {"description": "Orders", "content": {"application/json": {
            "schema": {"type": "array", "maxItems": 2, "items": {"$ref": "#/components/schemas/Order"}}}}}}
        }
      },
      "/orders/{orderId}": {
        "get": {
          "operationId": "getOrder",
          "parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "integer"}}],
          "responses": {"200": {"description": "An order", "content": {"application/json": {
            "schema": {"$ref": "#/components/schemas/Order"}}}}}
        }
      }
    },
    "components": {"schemas": {"Order": {
      "type": "object", "required": ["orderId", "total"],
      "properties": {
        "orderId": {"type": "integer"},
        "total": {"type": "number", "minimum": 0},
        "currency": {"type": "string", "enum": ["EUR", "USD"]}
      }
    }}}
  }
}
HTTP 201
[Asserts]
jsonpath "$.name" == "docs-orders"
jsonpath "$.state" == "live"
jsonpath "$.origin" == "api"
jsonpath "$.url" == "{{base_url}}/mock/docs-orders/api"
jsonpath "$.report.ok" == true
jsonpath "$.operations" count == 2
GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Operation" == "getOrder"
jsonpath "$.orderId" == 7
jsonpath "$.total" isNumber
FieldDefaultMeaning
specrequiredThe OpenAPI document: an object, or JSON / YAML text
nameslug of info.title1 to 40 characters a-z, 0-9 and -, not at either end; import, export, apply and reload are reserved. A taken default gets a -2, -3, ... suffix; a taken explicit name is a 409.
deploytrueRun the checks and make the draft live. false creates a draft only (its URL answers 503 until deployed).
forcefalseDeploy even when the checks report errors
enabledtrueThe on/off switch
strip_server_prefix, responses, security, operationsdefaultsThe mock's settings, the same structure as in rustybin.yaml

A deploy that the checks refuse answers 422 with the report; an invalid spec or invalid settings answer 400 with a message.

POST /_rustybin/mocks/import parses a spec without creating anything and returns what the console's wizard shows: title, version, suggested (free) name, server prefix, operations, security schemes, warnings and a check summary. The body is {"spec": ...} or the document itself (JSON or YAML text):

# A preview: nothing is created. The body may also be the spec itself
# (JSON or YAML text).
POST {{base_url}}/_rustybin/mocks/import
Content-Type: application/json
{
  "spec": {
    "openapi": "3.0.3",
    "info": {"title": "Orders API", "version": "1.0"},
    "servers": [{"url": "https://orders.example.com/api"}],
    "paths": {
      "/orders/{orderId}": {
        "get": {
          "operationId": "getOrder",
          "parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "integer"}}],
          "responses": {"200": {"description": "An order"}}
        }
      }
    }
  }
}
HTTP 200
[Asserts]
jsonpath "$.title" == "Orders API"
jsonpath "$.suggested_name" == "orders-api"
jsonpath "$.server_prefix" == "/api"
jsonpath "$.operations[0].id" == "getOrder"
jsonpath "$.operations[0].method" == "GET"
jsonpath "$.declares_security" == false
jsonpath "$.report.ok" == true

With the CLI

rustybin mock validate openapi.yaml                    # offline: parse and check
rustybin mock init openapi.yaml --name orders > rustybin.yaml
rustybin mock push --server http://localhost:8080 --token change-me
curl http://localhost:8080/mock/orders/...

See CLI for every subcommand.

The built-in sample

Every instance starts with a petstore mock (src/mocks/petstore.yaml, OpenAPI 3.0, server prefix /v1), so there is something to call on first run. It is not created in public mode; set RUSTYBIN_MOCKS_SAMPLE=false (or true) to change that. It answers from its examples, generates the pets that have none, and has one rule: pet id 0 answers the documented 404.

GET {{base_url}}/mock/petstore/v1/pets
HTTP 200
[Asserts]
header "Content-Type" == "application/json"
header "X-Rustybin-Mock" == "petstore"
header "X-Rustybin-Mock-Operation" == "listPets"
header "X-Rustybin-Mock-Source" == "example:three"
header "X-Total-Count" == "3"
jsonpath "$" count == 3
jsonpath "$[0].name" == "Rex"
# No example for this 200: the body is generated from the Pet schema, and
# `id` echoes the path parameter.
GET {{base_url}}/mock/petstore/v1/pets/42
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Operation" == "getPetById"
header "X-Rustybin-Mock-Source" == "generated"
jsonpath "$.id" == 42
jsonpath "$.name" isString
jsonpath "$.status" matches /^(available|pending|sold)$/
# The sample ships one rule: petId 0 answers the spec's 404 example.
GET {{base_url}}/mock/petstore/v1/pets/0
HTTP 404
[Asserts]
header "X-Rustybin-Mock-Source" == "rule:unknown pet example:notFound"
jsonpath "$.message" == "pet not found"
GET {{base_url}}/mock/petstore/v1/pets/1
Prefer: code=404
HTTP 404
[Asserts]
header "X-Rustybin-Mock-Source" == "prefer example:notFound"
jsonpath "$.code" == 404
POST {{base_url}}/mock/petstore/v1/pets
Content-Type: application/json
{"name": "Rex", "tag": "dog"}
HTTP 201
[Asserts]
header "Location" == "/v1/pets/4"
header "X-Rustybin-Mock-Validation" not exists
jsonpath "$.id" == 4

The sample can be edited, switched off and switched back on like any other mock; it is left out of GET /_rustybin/mocks/export (export it by name if you want a copy). Restarting the instance restores it.

URLs

URLWhat it is
/mock/{name}{prefix}{path}An operation of the mock: {path} is the spec path with its parameters filled in, {prefix} the path of the spec's first server URL (/v1 for servers: [{url: https://api.example.com/v1}], empty without servers). Any method.
/mock/{name}/_rustybin/openapi.json and .yamlThe deployed spec, with servers rewritten to the mock URL, ready to import into a gateway or an API client

strip_server_prefix: true serves the paths without the prefix (/mock/orders/orders/7 instead of /mock/orders/api/orders/7). Server URL variables are replaced by their defaults. The two spec URLs win over the operations (a spec path /_rustybin/openapi.json cannot be mocked) and are not captured.

What a mock URL answers when no operation responds, as application/problem+json and captured in the mock's traffic (except for an unknown name, which has no mock to capture it):

SituationStatus
No mock with that name404 {"error": "mock not found", ...}
Mock created but never deployed503
Mock switched off503, or responses.disabled_status (any 4xx or 5xx, for example 404)
No operation matches the path404, with the mock's base_path
The path matches but not the method405 with Allow
GET {{base_url}}/mock/petstore/v1/owners
HTTP 404
[Asserts]
header "Content-Type" == "application/problem+json"
jsonpath "$.detail" == "no operation of this mock matches GET /v1/owners"
jsonpath "$.base_path" == "/mock/petstore/v1"

PATCH {{base_url}}/mock/petstore/v1/pets
HTTP 405
[Asserts]
header "Allow" == "GET, POST"

GET {{base_url}}/mock/no-such-mock/anything
HTTP 404
[Asserts]
jsonpath "$.error" == "mock not found"
GET {{base_url}}/mock/petstore/_rustybin/openapi.json
HTTP 200
[Asserts]
jsonpath "$.info.title" == "Petstore"
jsonpath "$.servers" count == 1
jsonpath "$.servers[0].url" == "{{base_url}}/mock/petstore/v1"

GET {{base_url}}/mock/petstore/_rustybin/openapi.yaml
HTTP 200
[Asserts]
header "Content-Type" == "application/yaml"
body contains "url: {{base_url}}/mock/petstore/v1"

Every mock response carries X-Rustybin-Mock: <name>, and once an operation matched X-Rustybin-Mock-Operation: <operationId> (a stable id such as get_orders_id is made up when the spec has none). Successful mock responses also carry X-Rustybin-Mock-Source (how the response was chosen) and, when the request did not match the spec, X-Rustybin-Mock-Validation: failed. All four are exposed to browsers through CORS.

Fault injection (X-Rustybin-Delay, X-Rustybin-Fail) works on mocks like on every data-plane route.

Listing and inspecting mocks

GET {{base_url}}/_rustybin/mocks
HTTP 200
[Asserts]
jsonpath "$.mocks[*].name" contains "petstore"
jsonpath "$.limits.max_mocks" == 100
jsonpath "$.load_errors" isCollection

GET {{base_url}}/_rustybin/mocks/petstore
HTTP 200
[Asserts]
jsonpath "$.state" == "live"
jsonpath "$.origin" == "sample"
jsonpath "$.url" == "{{base_url}}/mock/petstore/v1"
jsonpath "$.spec.server_prefix" == "/v1"
jsonpath "$.operations[*].id" contains "getPetById"
jsonpath "$.credentials.api_key.keys[0]" == "demo-key"

The list gives, per mock: name, url, title, version, state (live, off or draft when never deployed), enabled, has_draft_changes, origin (api, file or sample), the operation count, security (enabled and mode), response mode, validate_requests, requests_total, last_hit, the live and draft revision numbers, a check summary and expires_at (public mode). It also returns the instance limits and load_errors (problems from rustybin.yaml or the mocks directory).

GET /_rustybin/mocks/{name} adds the draft's details: the spec summary and its warnings, the settings, every operation (method, path, statuses, whether it is secured, where its response comes from, its check status), the demo credentials, the deploy report summary and the revisions.

Draft, deploy, rollback

Every change (PATCH /_rustybin/mocks/{name}, PUT .../operations/{op}) goes to the mock's draft; the live revision keeps answering until you deploy. PATCH takes name (rename: the old URL stops working), spec (replace the document), strip_server_prefix, responses, security and operations. Each given section replaces the draft's section as a whole (a responses object without mode resets the mode to examples); sections you leave out are kept. "deploy": true publishes in the same call.

# Edits go to the draft: the live revision keeps answering until a deploy.
PATCH {{base_url}}/_rustybin/mocks/docs-orders
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"strip_server_prefix": true}
HTTP 200
[Asserts]
jsonpath "$.state" == "live"
jsonpath "$.has_draft_changes" == true
jsonpath "$.revisions[0].draft" == true

GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 200

POST {{base_url}}/_rustybin/mocks/docs-orders/deploy
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.deployed" == true
jsonpath "$.report.ok" == true
jsonpath "$.mock.has_draft_changes" == false

# The server prefix (/api) is gone from the mock URL.
GET {{base_url}}/mock/docs-orders/orders/7
HTTP 200
[Asserts]
jsonpath "$.orderId" == 7

POST /_rustybin/mocks/{name}/deploy runs the deploy checks on the draft and swaps it in atomically; check errors answer 422 with the report unless you add ?force=true. Deploying an unchanged draft is a no-op. The previous live revisions are kept (5, 2 in public mode): POST .../rollback makes the previous one live again, or the one named in {"revision": n} (revision numbers come from the details' revisions list). The draft follows the rollback.

POST {{base_url}}/_rustybin/mocks/docs-orders/rollback
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.rolled_back" == true

GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 200

On and off

POST .../disable switches a mock off without losing its configuration or traffic: its URL answers 503 (or responses.disabled_status), which gives gateway health checks and circuit breakers something to react to. POST .../enable switches it back on. DELETE /_rustybin/mocks/{name} removes the mock and its traffic.

POST {{base_url}}/_rustybin/mocks/docs-orders/disable
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.state" == "off"

GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 503
[Asserts]
header "Content-Type" == "application/problem+json"
jsonpath "$.detail" == "this mock is switched off"
jsonpath "$.mock" == "docs-orders"

POST {{base_url}}/_rustybin/mocks/docs-orders/enable
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.state" == "live"

GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 200
DELETE {{base_url}}/_rustybin/mocks/docs-orders
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.deleted" == "docs-orders"

GET {{base_url}}/mock/docs-orders/api/orders/7
HTTP 404

Traffic

Every call to a mock is captured, including the ones answered with 401, 400, 404, 405 or 503: method, path below the mock (path) and full path, query, headers, client IP, body (up to 64 KiB), the outcome (mocked, unauthorized, forbidden, invalid_request, not_found, method_not_allowed, disabled, not_deployed), the matched operation, the live revision, the response source, the validation issues, the auth result, the status, the response headers and body, and the duration.

GET {{base_url}}/_rustybin/mocks/docs-orders/requests
HTTP 200
[Asserts]
jsonpath "$.total" == 1
jsonpath "$.requests[0].method" == "GET"
jsonpath "$.requests[0].path" == "/api/orders/7"
jsonpath "$.requests[0].outcome" == "mocked"
jsonpath "$.requests[0].operation" == "getOrder"
jsonpath "$.requests[0].status" == 200
jsonpath "$.requests[0].validation" isEmpty
jsonpath "$.requests[0].response_body" contains "\"orderId\": 7"
[Captures]
seq: jsonpath "$.requests[0].seq"

GET {{base_url}}/_rustybin/mocks/docs-orders/requests/{{seq}}
HTTP 200
[Asserts]
jsonpath "$.full_path" == "/mock/docs-orders/api/orders/7"
RouteReturns
GET /_rustybin/mocks/{name}/requestsCaptured requests, newest first (?limit=, default 50)
GET /_rustybin/mocks/{name}/requests/{n}One request by sequence number
GET /_rustybin/mocks/{name}/requests/streamLive feed (Server-Sent Events, one request event per call, Last-Event-ID replays)
DELETE /_rustybin/mocks/{name}/requestsClear the captures (admin)

The oldest captures are evicted first when the log is full (see the limits below). Mock calls also appear in the request inspector like any other data-plane request.

Control-plane protection

Mock management lives on the control plane (/_rustybin/mocks*); the mocks themselves (/mock/*) are data plane and never need a control-plane token.

  • Every change (create, PATCH, PUT, deploy, rollback, enable, disable, delete, apply, reload, clearing traffic) follows the admin token rules: with RUSTYBIN_ADMIN_TOKEN set it needs Authorization: Bearer <token> or X-Rustybin-Admin-Token; without one it is open, except in public mode where it is refused (403), so a public instance is read-only for mocks.
  • With control-plane authentication (RUSTYBIN_CONTROL_AUTH=token or jwt), reading needs a token with the console scope and every POST, PUT, PATCH and DELETE needs the admin token or the admin scope, except the two read-only POST routes, import and .../validate, which the console scope may call.
# The run.sh server has an admin token: mutations without it are refused.
POST {{base_url}}/_rustybin/mocks/docs-orders/disable
HTTP 401
# Secured instance (control auth jwt): a console token reads mocks...
GET {{secured_url}}/_rustybin/mocks
Authorization: Bearer {{console_token}}
HTTP 200
[Asserts]
jsonpath "$.mocks[*].name" contains "petstore"

# ...but cannot change them (every POST, PUT, PATCH and DELETE needs admin).
POST {{secured_url}}/_rustybin/mocks/petstore/disable
Authorization: Bearer {{console_token}}
HTTP 403
[Asserts]
jsonpath "$.reason" == "insufficient_scope"

# The mocks themselves are data plane: no token.
GET {{secured_url}}/mock/petstore/v1/pets
HTTP 200

Limits

NormalPublic mode
Mocks per instance (sample and file mocks included)10050
Spec size1 MiB256 KiB
Operations per mock500200
Rules per operation2010
Configured response body256 KiB64 KiB
Previous revisions kept for rollback52
Captured requests per mock200 (1 MiB)50 (256 KiB)
Captured request body64 KiB64 KiB
delay_ms30 s10 s
Lifetime of mocks created through the console or APIunlimited24 hours

Mocks from rustybin.yaml, the mocks directory and the sample never expire. Other fixed caps: 20 headers per configured response, matches patterns up to 256 characters, 50 API keys, bearer tokens and basic users per mock, $ref chains up to 32 deep, a rustybin.yaml up to 8 MiB and 200 files in the mocks directory. GET /_rustybin/mocks returns the instance's limits as limits.

Control API

RoutePurpose
GET /_rustybin/mocksList mocks, limits and load errors
POST /_rustybin/mocksCreate (admin)
POST /_rustybin/mocks/importPreview a spec, create nothing
GET /_rustybin/mocks/{name}Details of the draft, revisions, credentials
PATCH /_rustybin/mocks/{name}Edit the draft (admin)
DELETE /_rustybin/mocks/{name}Delete (admin)
POST /_rustybin/mocks/{name}/enable, .../disableThe on/off switch (admin)
POST /_rustybin/mocks/{name}/deployCheck and publish the draft, ?force=true (admin)
POST /_rustybin/mocks/{name}/rollbackPrevious revision, or {"revision": n} (admin)
GET /_rustybin/mocks/{name}/reportDeploy checks of the draft
GET, PUT /_rustybin/mocks/{name}/operations/{op}One operation, or set its settings (admin)
GET /_rustybin/mocks/{name}/operations/{op}/sampleA valid request and response
POST /_rustybin/mocks/{name}/operations/{op}/validateValidate a body against the operation's schema
GET, DELETE /_rustybin/mocks/{name}/requests, .../requests/{n}, .../requests/streamTraffic
GET /_rustybin/mocks/{name}/export, GET /_rustybin/mocks/exportrustybin.yaml export
POST /_rustybin/mocks/applyApply a rustybin.yaml document, ?prune=true (admin)
POST /_rustybin/mocks/reloadRe-read the configured files (admin)
GET /_rustybin/mocks/config.schema.jsonJSON Schema of rustybin.yaml

{op} is the operationId (or the id Rustybin made up, shown in the details and in X-Rustybin-Mock-Operation). The routes are also in /openapi.json under the Mocks tag and in the console's API explorer.