API stubbing is the practice of replacing live API endpoints with controlled, deterministic responses. It matters when dependent systems are unavailable, when you need to test edge cases without hitting production, when rate limits or cost constraints block exploratory development, or when you’re verifying client behavior against specific server shapes without deploying a server.

Unlike mocking, which typically focuses on a single unit in isolation, stubbing often stands in for an entire service boundary. The stub accepts inbound requests and returns pre-recorded or generated responses, allowing the rest of the stack to proceed. When done well, it becomes a gate: the test passes only if the stub sees exactly what you expect. When done poorly, it silently swallows requests or returns canned data that doesn’t match the real contract, giving you a false sense of coverage.

Getting a stub running quickly

The fastest way to get a stub operating is with a tool that can read a response file and serve it on a port. mountebank (TBWG’s open-source service virtualization tool) is one real option. After installation, you can start a simple stub with the mb command:

mb --port 8082 --stub "{\"port\": 8082, \"protocol\": \"http\", \"responses\": [{\"is\": {\"port\": 8081}, \"then\": {\"file\": {\"name\": \"happy.json\"}}}]}"

The --stub flag accepts a JSON contract that tells mountebank to listen on port 8082, protocol http, and when any request arrives, compare its inbound port to 8081. If it matches, serve the contents of happy.json as the response body. The file response type supports plain text, JSON, or HTML—whatever the consumer expects.

If you don’t have mountebank installed, wiremock is a widely used Java-based alternative. Start it with:

java -jar wiremock-3.6.0.jar --port 8080 --root-dir stubs

WireMock reads files from --root-dir. Place a file named POST_user_created.json in the stubs directory with contents like:

{
  "status": 201,
  "headers": {
    "Content-Type": "application/json"
  },
  "jsonBody": {
    "id": "{{jsonPath \"$.id\"}}",
    "email": "{{jsonPath \"$.email\"}}"
  }
}

WireMock’s request matching is based on HTTP method, path, and body contents. A POST /users request whose body contains {"name":"Ada"} will return that JSON with a 201 status. Note that WireMock’s body matching uses JSON path extractors and regular expressions by default—if your inbound body is form-encoded or XML, you’ll need to configure a different matcher or preprocess the request.

For ad-hoc stubbing without installing anything, curl can serve a static response on a free port using a simple trick. Start a background process that reads from a file and echoes it:

while true; do cat response.json | nc -l 16744; done &

Then configure your client to hit localhost:16744. This won’t do any request matching—every request gets the same file—but it’s enough to unblock a test suite that just needs a body back. Kill the process with pkill -f 'nc -l 16744' when you’re done.

Defining response behavior

The core of any stub is the response definition: what gets returned, under what conditions, and with what status code. Real-world stubs converge on a few patterns.

Static file serving is the simplest. Both mountebank and wiremock support returning the exact bytes in a file. The file can be JSON, XML, CSV, or plain text. The stub does no transformation; it’s a passthrough. This is sufficient when the real server’s behavior is deterministic and you just need to avoid a network call. The sharp edge here is that if the real server adds headers (e.g., ETag, Last-Modified, request-id), the stub will not replicate them unless you explicitly include them in the file.

Template interpolation lets you inject dynamic values without a full server. WireMock’s {{jsonPath "$.id"}} syntax extracts a field from the inbound request body and places it into the response. Mountebank supports $$ placeholders that get replaced at match time. These are useful for verifying that a client passes the right identifiers, but they’re fragile: if the client sends a body missing the expected path, the stub either fails to match (wiremock) or returns a placeholder literal (mountebank), which can look like a successful response while silently corrupting downstream data.

Stateful matching is where stubs get tricky. Some tools let you match on request headers, query parameters, or cookie values. WireMock’s queryString and header blocks in the mapping JSON allow exact or regex matches. Mountebank’s matchers block can test any part of the inbound request. The failure mode is subtle: if you match on a header named X-Correlation-ID but the client sends X-Correlation-ID with a trailing space, the match fails and the stub falls through to a default response—or worse, returns a 501 “not implemented” if no default is configured. Always log what the stub actually received; many a test has passed while the real server saw a different value because the stub’s matching rule was narrower than assumed.

Response templating with randomness appears in tools like httpbin. Running httpbin.org/stub gives you a URL that, when hit, returns a JSON payload you define via a query parameter. For example:

curl "http://httpbin.org/stub?status=404&body={\"error\":\"not found\"}"

This is not a persistent stub—each invocation is independent—but it’s handy for quick experiments. The limitation is that httpbin is a public service; you cannot rely on it for CI isolation, and rate limits will kick in if your test suite is large.

Verifying stub interactions

Once a stub is running, you need to know whether your code actually hit it. Real tools provide request logging and assertion capabilities.

Mountebank writes every received request to a log file (default: mb.log in the working directory). Each entry includes the inbound method, path, headers, and body. You can tail this file during a test run:

tail -f mb.log | grep "POST /api/v1/users"

If your test sends a request and the log is empty, either the client is hitting a different port, the protocol mismatch (http vs https), or the stub’s is condition didn’t evaluate to true. Mountebank’s --debug flag adds verbose output about matcher evaluation, which is invaluable for diagnosing silent mismatches.

WireMock can be asked to verify that a certain mapping was invoked. After a test, run:

java -jar wiremock-3.6.0.jar --verify --port 8080

WireMock will exit with a non-zero status if any mapping that was expected (via --expect at startup) was not seen, or if any mapping was seen that wasn’t expected (via --no-differentiate). This makes wiremock a useful CI gate: the build fails if your code didn’t exercise the stub as assumed.

curl has no built-in verification, but you can combine it with jq. After a request, pipe the response body and check for expected fields:

response=$(curl -s http://localhost:8082/health)
echo "$response" | jq -e '.status == "ok"' || echo "verification failed"

This is rudimentary but works for simple contracts. The sharp edge: if the response is not valid JSON, jq will error out and the test will report failure even though the stub returned HTML or text. Always check curl's HTTP status code separately with -w "%{http_code}".

CI/CD integration

Stubbing in a pipeline requires three concerns: starting the stub, keeping the contract in sync, and tearing down.

Starting the stub can be done as a container. Both mountebank and wiremock have official Docker images:

docker run -d -p 8082:8082 --name my-stub mountebank/improbable-mb
docker run -d -p 8080:8080 --name my-wiremock wiremock/wiremock

In a CI job, you’d start the container, wait for the port to be responsive (a curl -s http://localhost:8082/health || exit 1 loop), run your tests, then docker stop my-stub. The contract lives alongside your code—in wiremock’s case, the JSON files under `stubs/\); in mountebank’s, the JSON contracts can be stored in a git repo and mounted into the container at runtime.

Keeping contracts in sync is the hardest part. A common pattern is to record real traffic and replay it. Mountebank has a --recording mode that captures inbound requests and writes them to a file. You can start it with:

mb --port 8082 --recording recordings.json

Run your integration tests against the stub. When done, recordings.json contains every request and the stub’s (default) responses. Review the file, prune unnecessary fields, and commit the cleaned version as your stub contract. WireMock offers a similar feature via its admin API: hit /__admin/mappings after a test session to export the mappings that were actually used.

Tearing down is usually automatic when the container stops, but if you’re running stubs as bare processes, make sure your CI teardown step kills them. A common failure mode is a zombie process that holds the port, causing the next pipeline job to fail with “address already in use.” Use docker kill or pkill -f mb reliably.

Sharp edges

Silent request dropping is the most dangerous failure mode. Both mountebank and wiremock will match a request to a stub only if all specified conditions are met. If you define a stub that matches POST /orders with a body containing {"item":"widget"}, and your test sends POST /orders with {"item":"gadget"}, the stub may return a 404 (not found) or, depending on configuration, a 501 (not implemented). Neither tool logs a warning when a request falls through without a matching stub—it just returns whatever default you configured, which is often an empty 200 OK or a generic error page. The consumer’s test passes because it received a response, but the response is wrong. Always configure a “fallback” or “default” mapping that returns a non-200 status, and verify in your test suite that the fallback was hit when expected.

Header confusion is another subtle issue. WireMock by default matches request headers case-insensitively but requires exact value match. Mountebank’s matchers can use regex or substring tests. If your real server sends Content-Type: application/vnd.api+json and your stub only expects application/json, the match fails. Conversely, if your stub uses a regex like /json/ and the real server sends application/hal+json, the match succeeds when it shouldn’t. Document the exact header values your stub expects, and write a test that sends a deliberately wrong header to confirm the stub rejects it.

State leakage between tests occurs when a stub’s internal state persists across test runs. WireMock’s mappings are in-memory by default; if you start a new container per job, this is fine. But if you reuse a container and don’t clear the mapping cache, a mapping from a previous test suite run may still be active, causing the new test’s assertions to fail unpredictably. Mountebank’s --reset flag clears all state on startup, which is why many CI configurations include mb --reset before running tests.

Port conflicts are operational, not semantic, but they break pipelines. Two stubs started on the same port will cause one to fail to bind. Use environment variables or Docker’s --publish to assign dynamic ports (-p 0:8082) and have your test suite discover the actual port at runtime via a health check endpoint rather than hardcoding 8082.

Response body mismatch is the most common source of false negatives. A stub may return a 200 OK with a JSON body, but if the body is missing a field your client asserts exists, the test fails at the assertion layer, not the stub layer. WireMock’s "jsonBody" field uses JSON path matching by default, which means a missing field is a non-match, not a mismatch. Mountebank’s then block serves the file verbatim; if the file is truncated or corrupted, the client receives malformed data and the error surface is the client’s JSON parser, not the stub. Always validate the stub’s response schema independently—jsonschema against a file, or jq checks—before asserting client behavior.

When to stop stubbing

Stubbing stops being useful when the cost of maintaining the stub exceeds the cost of hitting the real endpoint. If the real API has stable contracts and low latency, stubbing adds an indirection layer that slows down development more than it helps. If the real API changes weekly, your stub contracts will rot and erode confidence. Use stubs for bounded contexts: new features behind unmerged endpoints, third-party integrations you can’t control, or environments where rate limits prohibit exploratory testing. When the real service is available and the contract is well-documented, prefer integration tests against the actual endpoint; the confidence gain is worth the extra network call.

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.