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/42returns"id": 42). Rules, overrides and thePreferheader 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.yamlfile or a directory of specs at startup (Configuration as code), or with therustybin mockCLI.
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
- Open
/ui/#/mocks(for examplehttp://localhost:8080/ui/#/mocks). - 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. - 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.
- 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
| Field | Default | Meaning |
|---|---|---|
spec | required | The OpenAPI document: an object, or JSON / YAML text |
name | slug of info.title | 1 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. |
deploy | true | Run the checks and make the draft live. false creates a draft only (its URL answers 503 until deployed). |
force | false | Deploy even when the checks report errors |
enabled | true | The on/off switch |
strip_server_prefix, responses, security, operations | defaults | The 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
| URL | What 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 .yaml | The 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):
| Situation | Status |
|---|---|
| No mock with that name | 404 {"error": "mock not found", ...} |
| Mock created but never deployed | 503 |
| Mock switched off | 503, or responses.disabled_status (any 4xx or 5xx, for example 404) |
| No operation matches the path | 404, with the mock's base_path |
| The path matches but not the method | 405 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"
| Route | Returns |
|---|---|
GET /_rustybin/mocks/{name}/requests | Captured requests, newest first (?limit=, default 50) |
GET /_rustybin/mocks/{name}/requests/{n} | One request by sequence number |
GET /_rustybin/mocks/{name}/requests/stream | Live feed (Server-Sent Events, one request event per call, Last-Event-ID replays) |
DELETE /_rustybin/mocks/{name}/requests | Clear 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: withRUSTYBIN_ADMIN_TOKENset it needsAuthorization: Bearer <token>orX-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=tokenorjwt), reading needs a token with theconsolescope and everyPOST,PUT,PATCHandDELETEneeds the admin token or theadminscope, except the two read-onlyPOSTroutes,importand.../validate, which theconsolescope 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
| Normal | Public mode | |
|---|---|---|
| Mocks per instance (sample and file mocks included) | 100 | 50 |
| Spec size | 1 MiB | 256 KiB |
| Operations per mock | 500 | 200 |
| Rules per operation | 20 | 10 |
| Configured response body | 256 KiB | 64 KiB |
| Previous revisions kept for rollback | 5 | 2 |
| Captured requests per mock | 200 (1 MiB) | 50 (256 KiB) |
| Captured request body | 64 KiB | 64 KiB |
delay_ms | 30 s | 10 s |
| Lifetime of mocks created through the console or API | unlimited | 24 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
| Route | Purpose |
|---|---|
GET /_rustybin/mocks | List mocks, limits and load errors |
POST /_rustybin/mocks | Create (admin) |
POST /_rustybin/mocks/import | Preview 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, .../disable | The on/off switch (admin) |
POST /_rustybin/mocks/{name}/deploy | Check and publish the draft, ?force=true (admin) |
POST /_rustybin/mocks/{name}/rollback | Previous revision, or {"revision": n} (admin) |
GET /_rustybin/mocks/{name}/report | Deploy checks of the draft |
GET, PUT /_rustybin/mocks/{name}/operations/{op} | One operation, or set its settings (admin) |
GET /_rustybin/mocks/{name}/operations/{op}/sample | A valid request and response |
POST /_rustybin/mocks/{name}/operations/{op}/validate | Validate a body against the operation's schema |
GET, DELETE /_rustybin/mocks/{name}/requests, .../requests/{n}, .../requests/stream | Traffic |
GET /_rustybin/mocks/{name}/export, GET /_rustybin/mocks/export | rustybin.yaml export |
POST /_rustybin/mocks/apply | Apply a rustybin.yaml document, ?prune=true (admin) |
POST /_rustybin/mocks/reload | Re-read the configured files (admin) |
GET /_rustybin/mocks/config.schema.json | JSON 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.