Skip to content
denislituevPublic

About

Lightweight standalone mock HTTP server

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Repository files navigation

Mockd

CI Release crates.io downloads docs.rs license rust

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.yaml

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, OPTIONS
  • HEAD is answered by GET routes: 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: true to drop the connection after responding
  • Sequence responses (response.sequence:) for testing retry/polling
  • Permissive CORS via --cors (handles OPTIONS preflight, adds Access-Control-Allow-Origin: *)
  • Request logging via tracing (one structured line per request)

Installation

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-http

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:

mockd serve mocks.yaml

Try it:

curl http://localhost:8080/users/42
# {"id":42,"name":"User 42"}

Validate a config without serving:

mockd validate mocks.yaml

Parse 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 --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/OPTIONS
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                          # 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; matches and contains are case-sensitive.

  • Query values and path parameters are percent-decoded before matching and templating: ?name=John%20Doe matches name: 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 key matches/contains and 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, body conditions never match and {{body.*}} templates render null.

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

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=/"

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: { 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.

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 how id: "{{path.id}}" becomes 42 rather 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: *.
  • OPTIONS preflight requests (i.e. they carry Access-Control-Request-Method) are answered with 204 No Content without consulting the routes. The Access-Control-Allow-Headers value is echoed from the request. Plain (non-preflight) OPTIONS requests 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).

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 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.

Editor support (JSON Schema)

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.

Architecture

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.

Running the tests

cargo test

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.

Limitations

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.

Roadmap

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.

License

Dual-licensed under either of

at your option.

About

Lightweight standalone mock HTTP server

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages