Mockd
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.
It aims to be simpler than WireMock / MockServer, but convenient for the day-to-day work of microservice developers.
Features
- HTTP methods:
GET,POST,PUT,PATCH,DELETE - Path parameters:
/users/{id} - Request matching on query parameters, headers (case-insensitive) and JSON body (subset match)
- 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)
Installation
From source (any platform with a Rust toolchain):
Once published to crates.io (the package name is mockd-http; the binary
is still installed as mockd):
Pre-built binaries for Linux (amd64/arm64), macOS (amd64/arm64) and Windows (amd64) are published on the releases page.
Quick start
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:
Try it:
# {"id":42,"name":"User 42"}
Validate a config without serving:
For browser/SPA testing, enable CORS:
See examples/users.yaml for a more complete example
covering matching, templates, delays and errors.
Configuration reference
Top level
| field | type | default | description |
|---|---|---|---|
listen |
string | :8080 |
Socket address to bind |
routes |
list[route] | [] |
Routes, first match wins |
Route
| field | type | description |
|---|---|---|
method |
enum | GET/POST/PUT/PATCH/DELETE |
path |
string | Path pattern, e.g. /users/{id} |
when |
match? | Optional request matcher (see below) |
response |
response | Response produced when the route matches |
when (request matcher)
All conditions are optional; all present conditions must be satisfied.
when:
query:
role: admin
headers:
X-Tenant-Id: tenant-a
body:
username: admin
query: required query parameters (exact match).headers: required headers (matched case-insensitively).body: a JSON object 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.
response
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 |
Sequence responses
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:
- status: 500
body:
- status: 200
body:
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.
Templating
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.
CORS
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.
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).
Logging
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 RUST_LOG=mockd=debug
Architecture
Mockd is a single crate split into focused modules:
src/
├── main.rs # CLI (clap): `serve` and `validate`
├── 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.
Running the tests
Unit 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.
Roadmap
The following are planned for future releases, in rough priority order:
- Stateful responses (
state:) - OpenAPI import (
mockd import openapi.yaml) - Request recording / replay
The following are explicitly out of scope for now: GUI/Web UI, Kubernetes operator, gRPC, GraphQL, state machines.
See CHANGELOG.md for what is included in this release.
License
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.