Mockd is a lightweight standalone mock HTTP server for local development, integration tests and CI/CD. You describe your API with a declarative YAML config — no code required — and mockd serves it.
mockd serve mocks.yamlIt aims to be simpler than WireMock / MockServer, but convenient for the day-to-day work of microservice developers.
- HTTP methods:
GET,POST,PUT,PATCH,DELETE,OPTIONS HEADis answered byGETroutes: same status and headers, no body- Path parameters:
/users/{id} - Request matching with exact, substring (
contains) and regex (matches) matchers on query parameters, headers and JSON body fields - Response bodies as JSON, with templating:
{{path.id}}{{query.role}}{{header.X-Tenant-Id}}{{body.user.name}}(dot navigation through the request body)- helper functions:
{{uuid}},{{now}},{{randomInt(min,max)}}
- Custom status codes and headers
- Artificial delays (
delay: 2s) for timeout testing close_connection: trueto drop the connection after responding- Sequence responses (
response.sequence:) for testing retry/polling - Permissive CORS via
--cors(handlesOPTIONSpreflight, addsAccess-Control-Allow-Origin: *) - Request logging via
tracing(one structured line per request)
From source (any platform with a Rust toolchain):
cargo install --path .Once published to crates.io (the package name is mockd-http; the binary
is still installed as mockd):
cargo install mockd-httpPre-built binaries for Linux (amd64/arm64), macOS (amd64/arm64) and Windows (amd64) are published on the releases page.
Create mocks.yaml:
listen: ":8080"
routes:
- method: GET
path: /users/{id}
response:
status: 200
body:
id: "{{path.id}}"
name: "User {{path.id}}"Start the server:
mockd serve mocks.yamlTry it:
curl http://localhost:8080/users/42
# {"id":42,"name":"User 42"}Validate a config without serving:
mockd validate mocks.yamlParse errors point to the exact place in the config, e.g.
routes[0].when.query.email: invalid matcher: .... validate also warns
about routes that can never match because an earlier route shadows them.
Exit codes: 0 success, 1 config or runtime error, 2 CLI usage error
— convenient for CI scripts.
For browser/SPA testing, enable CORS:
mockd serve mocks.yaml --corsSee examples/users.yaml for a more complete example
covering matching, templates, delays and errors.
| field | type | default | description |
|---|---|---|---|
listen |
string | :8080 |
Socket address to bind |
routes |
list[route] | [] |
Routes, first match wins |
| field | type | description |
|---|---|---|
method |
enum | GET/POST/PUT/PATCH/DELETE/OPTIONS |
path |
string | Path pattern, e.g. /users/{id} |
when |
match? | Optional request matcher (see below) |
response |
response | Response produced when the route matches |
All conditions are optional; all present conditions must be satisfied.
when:
query:
role: admin # exact match
email:
matches: "^[^@]+@example\\.com$" # regular expression
name:
contains: alice # substring
headers:
X-Tenant-Id: tenant-a
X-Environment:
contains: staging
body:
username: admin
email:
matches: ".*@example\\.com$"| plain string | matches: |
contains: |
|
|---|---|---|---|
query |
exact match | regular expression | substring |
headers |
exact match | regular expression | substring |
body |
literal subset equality | applies to string values | applies to string values |
-
query/headers: each value is one of:- a plain string — exact match;
matches: <regex>— regular expression (not anchored, use^...$for a full match;(?i)for case-insensitivity);contains: <substring>— substring match.
A matcher object must have exactly one key; anything else is a config error.
-
Header names and exact header values are matched case-insensitively;
matchesandcontainsare case-sensitive. -
Query values and path parameters are percent-decoded before matching and templating:
?name=John%20Doematchesname: John Doe(+stays a literal plus sign). A query parameter repeated in the URL (?role=a&role=b) matches by its last value. -
body: a JSON pattern that must be a subset of the request body. Every field you list must be present and equal; extra fields in the request are ignored. Arrays must match element-by-element with the same length. Inside the pattern, an object with exactly one keymatches/containsand a string value is a matcher operator applied to the corresponding string value of the request body (as in the example above). Objects with more keys — or non-string values — are literal. Operators never match a non-string value in the request body. -
If the request body is not valid JSON,
bodyconditions never match and{{body.*}}templates rendernull.
A route's response can be either a single response or a sequence of
responses (see Sequence responses below).
| field | type | default | description |
|---|---|---|---|
status |
u16 | 200 |
HTTP status code |
headers |
map | {} |
Response headers |
body |
any/json | — | JSON body, may contain template expressions |
delay |
duration | — | e.g. 2s, 250ms, 1m 30s |
close_connection |
bool | false |
Send Connection: close and close after responding |
A headers value can be a single string or a list to emit the same header
multiple times (e.g. several Set-Cookie headers):
response:
status: 200
headers:
Set-Cookie:
- "session=abc; Path=/"
- "theme=dark; Path=/"Wrap multiple responses in response.sequence: to make mockd return each one
in order on successive calls. The last item is sticky: it is repeated on
every call once the previous items have been exhausted.
- method: GET
path: /flaky
response:
sequence:
- status: 500
body: { error: transient }
- status: 500
body: { error: transient }
- status: 200
body: { ok: true }Useful for testing retry logic, polling, pagination and any client behavior that depends on a sequence of states.
A single response: {...} is equivalent to a one-element sequence.
Template expressions go inside string values in body:
body:
id: "{{path.id}}"
role: "{{query.role}}"
tenant: "{{header.X-Tenant-Id}}"
label: "user-{{path.id}}"
echoed_name: "{{body.user.name}}"
request_id: "{{uuid}}"
created_at: "{{now}}"
priority: "{{randomInt(1,5)}}"Lookups:
| Namespace | Example | Meaning |
|---|---|---|
path |
{{path.id}} |
A captured path parameter |
query |
{{query.role}} |
A query parameter |
header |
{{header.X-Foo}} |
A request header (case-insensitive name) |
body |
{{body.user.id}} |
Dot-navigate the parsed JSON request body; |
| numeric segments index into arrays |
Helper functions (no namespace):
| Expression | Returns |
|---|---|
{{uuid}} |
A fresh UUIDv4 string |
{{now}} |
Current UTC time as ISO 8601 (...Z) |
{{randomInt(min,max)}} |
Random integer in the inclusive [min, max] range |
{{random}} |
A random 64-bit integer (whole range) |
Coercion rules:
- When the whole string value is a single expression, the result is coerced
to the best-fitting JSON type (
42→ number,true→ bool,null→ null, otherwise string). This is howid: "{{path.id}}"becomes42rather than"42". - When the expression is part of a larger string, it is interpolated as text.
- Missing/unknown variables resolve to JSON
null(whole-string) or an empty string (interpolation). Helper functions always resolve to a value.
Pass --cors to enable permissive cross-origin support:
- Every response gets
Access-Control-Allow-Origin: *. OPTIONSpreflight requests (i.e. they carryAccess-Control-Request-Method) are answered with204 No Contentwithout consulting the routes. TheAccess-Control-Allow-Headersvalue is echoed from the request. Plain (non-preflight)OPTIONSrequests are routed normally.
This is intended for local development where the mock server and the frontend
run on different origins (e.g. localhost:8080 mock + localhost:3000 Vite).
mockd logs every request to stderr via tracing. The default level is info
(one structured line per request). Tune with the standard RUST_LOG
environment variable:
RUST_LOG=mockd=warn mockd serve mocks.yaml # quieter
RUST_LOG=mockd=debug mockd serve mocks.yaml # verbose (includes delays)At the debug level mockd also explains route selection: why each candidate
route was skipped (method mismatch, path mismatch, or the exact when
condition that failed, e.g. query "role": expected "admin") and which
route matched. Useful when a request unexpectedly returns 404. Actual
request values are never logged for query parameters and body fields (type
mismatches report the JSON type instead), and sensitive headers
(Authorization, Cookie, ...) are reported without their values.
A JSON Schema for the configuration file is published at:
https://denislituev.github.io/mockd/schema.json
Add this comment to the top of your mocks.yaml to get autocompletion,
hover-documentation and inline validation in editors that support the
yaml-language-server convention (VS Code with the Red Hat YAML extension,
Zed, IntelliJ, Neovim):
# yaml-language-server: $schema=https://denislituev.github.io/mockd/schema.json
listen: ":8080"
routes:
# ...The schema is generated from the Rust types in src/config.rs
via schemars. Regenerate it with
mockd generate; a unit test guards against drift between the committed
schema and the code.
Mockd is a single crate split into focused modules:
src/
├── main.rs # CLI (clap): `serve`, `validate` and `generate`
├── lib.rs # library root
├── config.rs # domain models + YAML loading
├── router.rs # request matching (server-agnostic)
├── template.rs # {{...}} rendering
└── server.rs # Axum HTTP layer
The router is deliberately independent of the HTTP server, which makes it
cheap to unit-test and reuse.
cargo testUnit tests live next to the code (#[cfg(test)] modules); end-to-end tests are
in tests/integration.rs and spin up a real server on
an ephemeral port.
Request bodies are parsed as JSON; non-JSON bodies disable body: matching
and {{body.*}} template lookups. HTTPS is not supported — terminate TLS
at a reverse proxy if needed.
The 1.0 milestone is complete: the YAML configuration format and the library API are considered stable.
Out of scope: GUI/Web UI, database, OpenAPI import, gRPC, GraphQL, WebSockets, proxy/passthrough, plugin systems.
See CHANGELOG.md for what is included in this release.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.