Mimic is a fast, lightweight HTTP mock server built with Rust and Axum. Perfect for testing, development, and API prototyping. Define your mock responses in simple JSON files and let Mimic handle the rest.
- Blazing Fast - Built with Rust and Axum for maximum performance
- Ultra Lightweight - Only 1.66 MiB memory usage
- File-Based Configuration - Define mocks in simple JSON or YAML files, mixed freely in one directory
- Hot Reload - Changes to mock files are reflected immediately
- Docker Ready - Pre-built images available on Docker Hub
- Well Tested - 60+ unit tests with high code coverage
- Easy to Use - Simple configuration, no complex setup
- Configurable Body Consumption - Control request body handling per endpoint
- File Upload Support - Handle multipart/form-data with
consume_bodyoption - Advanced Matching - Match on path parameters, query params, headers, and request body
- Path Parameters -
/users/:idand/users/{id}syntax, one mock covers every value - Dynamic Response Templating - Echo path, query, header, and body fields back into responses with
{{path.x}}syntax - File-Backed Responses - Serve a PDF, PNG, CSV, or XML fixture from disk with
response_file, bytes intact - Faker Data Generators - Fresh random values on every call with
{{faker.uuid}},{{faker.name}},{{faker.int min=1 max=100}} - OpenAPI Import - Generate a whole mocks directory from an OpenAPI 3.x spec with
mimic import-openapi ./spec.yaml - Scenario Tags - Keep happy-path and error mocks side by side and switch between them with
MIMIC_ACTIVE_TAGSorPOST /admin/scenario - Built-in CORS -
MIMIC_CORS=trueanswersOPTIONSpreflights automatically and adds the allow-origin header to every mock β no per-endpoint CORS files - Admin Dashboard - Inspect loaded mocks, read why a request didn't match, and see the exact response served β at
/admin/dashboard - Proxy / Record-and-Replay - Forward unmatched requests to a real upstream and optionally save the response as a new mock with
MIMIC_PROXY_UPSTREAMandMIMIC_RECORD_UPSTREAM - Typed Template Casts -
{{number:path.id}},{{bool:query.active}},{{json:body.user}}render real JSON types instead of quoted strings - Configurable Bind Address - Restrict the server to localhost or a specific interface with
MIMIC_BIND_ADDRESSinstead of always listening on every interface
Mimic is incredibly efficient and lightweight:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
b056ebec5099 mimic 0.02% 1.66MiB / 6.238GiB 0.03% 17.1kB / 34.6kB 0B / 0B 17
Key Metrics:
- Memory Usage: Only 1.66 MiB (yes, megabytes!)
- CPU Usage: 0.02% at idle
- Startup Time: < 1 second
- Response Time: < 10ms for most requests
Perfect for resource-constrained environments, CI/CD pipelines, and local development.
Pull the latest image from Docker Hub:
docker pull ragilhadi/mimic:latestRun with your mock files:
docker run -d \
--name mimic \
-p 8080:8080 \
-v $(pwd)/mocks:/app/mocks:ro \
ragilhadi/mimic:latestTest it:
curl http://localhost:8080/health- Create a
docker-compose.yml:
services:
mimic:
image: ragilhadi/mimic:latest
container_name: mimic
ports:
- "8080:8080"
volumes:
- ./mocks:/app/mocks:ro
environment:
- PORT=8080
- RUST_LOG=info
restart: unless-stopped- Start the service:
docker compose up -dCreate a .env file to customize your setup:
# Port configuration (default: 8080)
PORT=8080
# Address to bind to (default: 0.0.0.0 β every interface). Set to 127.0.0.1
# to restrict access to localhost. IPv4 and IPv6 literals are both accepted
# (e.g. ::1, ::). An unset value keeps today's behavior; a value that's set
# but doesn't parse as an address fails startup rather than falling back to
# the default. See "Binding address" below.
MIMIC_BIND_ADDRESS=0.0.0.0
# Logging level: trace, debug, info, warn, error (default: info)
RUST_LOG=info
# Directory (or single JSON file) mocks are read from.
# Default: /app/mocks when it exists (the Docker image), else ./mocks.
MIMIC_MOCKS_DIR=./mocks
# Maximum request body Mimic will buffer, in bytes (default: 10485760 = 10 MB)
MIMIC_MAX_BODY_SIZE=10485760
# How many requests the admin request log keeps (default: 1000, 0 = unbounded)
MIMIC_MAX_LOG_ENTRIES=1000
# How much of each request/response body the log stores, in bytes
# (default: 65536 = 64 KB, 0 = store whole bodies)
MIMIC_MAX_RECORDED_BODY=65536
# Scenario tags active at startup, comma-separated (default: unset = all
# mocks matchable). See "Tagged Mock Groups" below.
MIMIC_ACTIVE_TAGS=happy-path,smoke-test
# Where the health check is served (default: /health). Empty = don't serve it,
# freeing the path for a mock. See "Reserved endpoints" below.
MIMIC_HEALTH_PATH=/health
# Prefix the admin API is mounted under (default: /admin).
MIMIC_ADMIN_PREFIX=/admin
# Switch the admin API off entirely (default: false)
MIMIC_DISABLE_ADMIN=false
# Bearer token the admin API requires (default: unset = no authentication)
MIMIC_ADMIN_TOKEN=
# Body field names whose values are replaced with [REDACTED] in the request
# log, comma-separated. Empty stores bodies verbatim. See "What the request
# log keeps" below for the default list.
MIMIC_REDACT_BODY_FIELDS=password,token,secret,api_key
# Store no request/response bodies in the log at all (default: false)
MIMIC_DISABLE_BODY_LOG=false
# Largest file a mock may serve with `response_file`, in bytes
# (default: 10485760 = 10 MB, 0 = no limit). See "File-Backed Responses" below.
MIMIC_MAX_RESPONSE_FILE=10485760
# Built-in CORS (default: off β no response gains a header, OPTIONS still 404s).
# See "Built-in CORS" below.
MIMIC_CORS=false
MIMIC_CORS_ORIGINS=*
MIMIC_CORS_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
MIMIC_CORS_HEADERS=*
MIMIC_CORS_CREDENTIALS=false
MIMIC_CORS_MAX_AGE=600
# Proxy an unmatched request to a real upstream instead of 404ing (default:
# unset = unchanged 404 behavior). See "Proxy / Record-and-Replay" below.
MIMIC_PROXY_UPSTREAM=https://api.example.com
# Record proxied responses as new mock files, so the next identical request
# is replayed from disk (default: false)
MIMIC_RECORD_UPSTREAM=true
# How long to wait for the upstream before falling back to a 404, in
# milliseconds (default: 5000)
MIMIC_PROXY_TIMEOUT_MS=5000Log Levels:
trace- Very detailed debuggingdebug- Detailed debugginginfo- General information (recommended)warn- Warnings onlyerror- Errors only
By default Mimic listens on 0.0.0.0 β every network interface β which is
convenient on a laptop but means the mock server, its request log, and its
admin API are reachable from anywhere that can route to the machine. On a
shared box or a machine with a public IP, that's more exposure than most setups
intend. MIMIC_BIND_ADDRESS restricts it:
MIMIC_BIND_ADDRESS=127.0.0.1 mimic # only this machine can reach it
MIMIC_BIND_ADDRESS=::1 mimic # IPv6 loopback
MIMIC_BIND_ADDRESS=:: mimic # every IPv6 interface- Unset behaves exactly as before β
0.0.0.0, every interface. - IPv4 and IPv6 literals are both accepted, since it's parsed as a bare
address, not a
host:portpair β don't append a port. - An unparsable value fails startup rather than silently falling back to
0.0.0.0: binding wider than what was asked for is the one outcome this setting exists to prevent, so a typo is reported as a startup error instead of a server that came up more exposed than intended. - The bound address is always visible: it's printed in the startup log
next to the port, and reported by
GET /healthasbind_address, so what a running instance is actually listening on is never a guess. - Inside Docker, leave it at
0.0.0.0.127.0.0.1there means "this container", not "this host" βdocker run -p/docker-composeport publishing can no longer reach it. Restrict access at the host or network level instead (bind the published port to127.0.0.1:8080:8080, or don't publish it at all).
If Mimic sits on a shared machine or a box with a public IP, MIMIC_BIND_ADDRESS=127.0.0.1
(or a firewall rule, or an auth layer in front) is what keeps it from being
remotely reachable at all β see What the request log keeps
for what's exposed if it is reached.
Mimic reads mock definitions from JSON or YAML files, searching the directory
it resolves at startup and every subdirectory beneath it. .json, .yaml,
and .yml files all define the same MockConfig shape β pick per file
whichever reads better, and mix both in one directory freely.
Where it reads from, in order:
MIMIC_MOCKS_DIR, if set β a directory, or a single.json/.yaml/.ymlfile. Used verbatim: a path that doesn't exist is reported as missing rather than silently replaced by a default./app/mocks, if it exists β the Docker image's mount point, so everydocker run -v $(pwd)/mocks:/app/mockscommand below resolves exactly there../mocks, relative to the working directory β whatcargo run,make dev, an installed binary, or a release build picks up from a clone of this repo.
The directory Mimic actually resolved is logged at startup, and a run that registers no mocks says which of the two reasons applies β the directory isn't there, or it's there and holds no mock files:
INFO mimic: Configuration:
INFO mimic: Mocks directory: ./mocks (default, relative to the working directory)
INFO mimic: Loaded 31 mock(s)
Mock File Structure:
{
"method": "GET",
"path": "/users",
"status": 200,
"response": {
"users": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com"
}
]
}
}Fields:
method- HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)path- URL path (e.g.,/users,/api/v1/products)status- HTTP status code, 100β599 (200, 201, 404, 500, etc.). A value outside that range can't be put on the wire; Mimic serves200 OKinstead, warns at load time naming the file, and marks the mock"servable": falseinGET /admin/mocksβ the request log always records what was actually served, never the out-of-range number from the file.response- JSON response body (can be object, array, or null)response_file- (Optional) Serve the body from a file next to the mock instead ofresponse; see File-Backed Responsestemplate- (Optional) Boolean enabling{{...}}templating inside aresponse_filebody (default:false)consume_body- (Optional) Boolean to control request body consumption (default:false)true- Consume request body (required for file uploads, multipart/form-data)false- Skip body consumption (faster, default behavior)
The same mock, as YAML (.yaml or .yml β either extension works):
# Comments are why this one is YAML: worth documenting, not worth an escaped
# multi-line string.
method: GET
path: /users
status: 200
response:
users:
- id: 1
name: Alice Johnson
email: alice@example.comYAML's block scalars (|) are the other reason to reach for it β a
multi-line HTML/XML body with no \n escaping:
method: GET
path: /docs
status: 200
response:
html: |
<html>
<body>Static docs page</body>
</html>JSON stays the default and first-class format β every example in this README
that's shown as .json works exactly as written; YAML is an alternative for
files where either of the above is worth it, not a replacement.
Mimic answers a handful of routes itself, and they are matched ahead of the
mock set. A mock declaring one of these loads normally, is listed by
GET /admin/mocks, and then never serves a request:
| Reserved | Method(s) |
|---|---|
/health |
GET |
/admin/dashboard |
GET |
/admin/requests |
GET, DELETE |
/admin/mocks |
GET |
/admin/sequences |
GET |
/admin/sequences/reset |
POST |
/admin/scenario |
GET, POST |
Only these exact method + path pairs are reserved. POST /health and
GET /admin/users reach the mock set normally and can be mocked.
A collision is reported rather than left to be discovered:
- the loader warns at startup, and again whenever hot reload introduces one,
naming the file β
mocks/health_down.json declares GET /health, which is reserved by Mimic's health check and will never be served; GET /admin/mocksreports the mock with"reachable": falseand anunreachable_reason, and the dashboard shows it as an unreachable badge β so a permanenthits: 0is distinguishable from "nothing has called it yet".
If your API genuinely owns these paths, take them back:
MIMIC_HEALTH_PATH= mimic # no health check; GET /health is yours
MIMIC_HEALTH_PATH=/_mimic/health mimic # or move it out of the way
MIMIC_ADMIN_PREFIX=/_mimic mimic # admin API at /_mimic/mocks, etc.
MIMIC_DISABLE_ADMIN=true mimic # no admin API at allWhatever is left reserved is logged at startup, and a freed route is a normal mock path from that moment on.
Mimic rescans the mocks directory every 2 seconds and picks up changes without a restart. Failures are isolated per file: if one file has invalid JSON β an editor mid-save, a teammate's work-in-progress in a shared mocks directory β every other file in that cycle still applies. A single typo can no longer block unrelated mock changes from taking effect.
A cycle where nothing changed does no file I/O. Each mock file β and each
response_file it references β is fingerprinted by modified time and size;
unchanged since the last cycle means it's neither re-read nor re-parsed, and
the mock store's write lock isn't even taken. Editing a file, adding one,
deleting one, or renaming one is still picked up within the same 2-second
window it always was, and editing a response_file fixture is detected even
when the mock file that references it isn't touched. A symlink that resolves
back into the mocks tree β directly, or through a shared-fixtures setup in a
monorepo β is walked once and reported, rather than registering its mocks
over and over on every cycle.
Change detection is a dependency-free stat-based check rather than
filesystem-event watching (notify or similar): it costs at most one stat
per file per idle cycle, needs no OS-specific watch API, and degrades the same
way on every platform and every filesystem (including inside a container with
a bind-mounted mocks directory, where inotify events don't always cross the
mount). The trade-off is a reload triggered by a stat sweep rather than an
instantaneous filesystem event β invisible at the 2-second cadence Mimic
already reloads on.
The broken file's own route is not dropped, either. It keeps serving its last successfully-loaded response until the file parses again, so routes don't flap in and out of existence while somebody is editing. Each failure is logged with the file name and parse error, plus a summary of how many endpoints were applied and how many were carried forward.
Deletions still take effect normally: on a clean cycle (no parse errors) the mock set is replaced outright, so removing a file removes its route.
Sequence positions and hit counts belong to the mock's file, not to its
position in the list of mocks sharing a METHOD:path. Across a reload:
- Editing a file β including editing its
sequenceβ leaves that mock's position and hit count where they were. Reset explicitly withPOST /admin/sequences/resetwhen you want a clean run. - Adding a mock under a
METHOD:paththat already has one does not disturb the existing mock's counters, whichever order the two files sort in. - Deleting a file drops its counters. They are not carried over to whatever is loaded next.
- A file that stops parsing keeps both its route and its counters, since its last-known-good response is still being served.
Mock files load in a fixed order β depth-first, alphabetical by full path β so
which of several mocks registered for the same METHOD:path wins a tie is a
function of their file names and nothing else. It does not vary between
filesystems, between a fresh clone and a rebuilt image, or after an unrelated
file is added to the directory.
File: mocks/get_users.json
{
"method": "GET",
"path": "/users",
"status": 200,
"response": {
"users": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "admin"
},
{
"id": 2,
"name": "Bob Smith",
"email": "bob@example.com",
"role": "user"
}
],
"total": 2
}
}Usage:
curl http://localhost:8080/usersFile: mocks/post_login.json
{
"method": "POST",
"path": "/login",
"status": 200,
"response": {
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com"
},
"expiresIn": 3600
}
}Usage:
curl -X POST http://localhost:8080/loginFile: mocks/get_error.json
{
"method": "GET",
"path": "/error",
"status": 500,
"response": {
"error": "Internal Server Error",
"message": "Something went wrong",
"code": "ERR_500"
}
}Usage:
curl http://localhost:8080/errorFile: mocks/delete_user.json
{
"method": "DELETE",
"path": "/users/123",
"status": 204,
"response": null
}Usage:
curl -X DELETE http://localhost:8080/users/123File: mocks/ocr_image.json
{
"method": "POST",
"path": "/ocr-image",
"status": 200,
"consume_body": true,
"response": {
"status": "SUCCESS",
"text": "Extracted text from image",
"confidence": 0.95,
"detected_text": [
{
"text": "Hello World",
"bbox": [10, 20, 100, 40]
}
]
}
}Usage:
# Upload image file
curl -X POST http://localhost:8080/ocr-image \
-F "image=@document.jpg" \
-F "language=en"Note: Set consume_body: true for endpoints that handle:
- File uploads (images, documents, etc.)
- Multipart/form-data requests
- Large request payloads
Without consume_body: true, clients may encounter "Broken Pipe" errors when sending large files.
Mimic supports advanced request matching beyond just HTTP method and path. You can match requests based on path parameters, query parameters, headers, and request body content.
A literal path only matches one exact URL, which gets impractical fast for REST-style resources β mocking GET /users/1, GET /users/2, GET /users/3 would otherwise need a separate file per id. Use named path parameters instead, and a single mock covers every value in that segment.
Both :id (Express-style) and {id} (OpenAPI-style) syntax are supported:
File: mocks/advanced/get_user_by_id.json
{
"method": "GET",
"path": "/users/:id",
"status": 200,
"response": {
"id": "{{path.id}}",
"name": "Mock User"
}
}curl http://localhost:8080/users/42
# { "id": "42", "name": "Mock User" }Multiple parameters, and nested resources, work the same way:
{
"method": "DELETE",
"path": "/orgs/{org}/repos/{repo}",
"status": 204,
"response": null
}Semantics:
- Captured values are available for response templating as
{{path.id}}. - An exact path always wins over a pattern when both could match β e.g. if
/users/42and/users/:idare both defined, a request for/users/42hits the exact mock and everything else falls through to the pattern. - Exact-path lookups stay O(1); the pattern scan only runs when the exact lookup matched nothing, so mocks with no path parameters see no performance change. Each path template is compiled to a regex once per process and reused, never recompiled per request.
- A sequence on a pattern mock advances a single shared counter across every value of the parameter (e.g.
/items/1and/items/2progress the same sequence for/items/:id), not one counter per resolved id.
Match requests based on URL query string parameters:
File: mocks/get_search.json
{
"method": "GET",
"path": "/search",
"status": 200,
"query_params": {
"params": {
"q": "test",
"page": "1"
},
"strict": false
},
"response": {
"results": [
{"id": 1, "title": "Test Result 1"},
{"id": 2, "title": "Test Result 2"}
],
"query": "test",
"page": 1
}
}Usage:
# Matches - exact params
curl "http://localhost:8080/search?q=test&page=1"
# Matches - extra params ignored (strict=false)
curl "http://localhost:8080/search?q=test&page=1&extra=value"
# Doesn't match - wrong value
curl "http://localhost:8080/search?q=wrong&page=1" # Returns 404Advanced Query Patterns:
{
"query_params": {
"params": {
"page": {"regex": "^[0-9]+$"},
"limit": {"regex": "^(10|20|50|100)$"},
"status": {"any": null}
},
"strict": false
}
}- Exact match:
"param": "value" - Regex match:
"param": {"regex": "^pattern$"} - Any value:
"param": {"any": null}(param must exist) - Strict mode:
"strict": true(rejects extra params β a repeated key still counts as one param, not one per value)
Repeated keys (?tag=a&tag=b, ?ids=1&ids=2) β the standard encoding for
a list-valued parameter, and what URLSearchParams, axios, Go's url.Values,
and an OpenAPI explode: true array parameter all emit:
"param": "value"and{{query.param}}both read the parameter's first value only β a mock written before repeated keys were matchable keeps meaning exactly what it always did."param": {"contains": "value"}matches if any occurrence of the key equalsvalue."param": {"list": ["a", "b"]}matches only the exact, ordered set of values the key carried β?tag=a&tag=bmatches["a", "b"], not["b", "a"]or["a"]alone.GET /admin/requestsand the dashboard report every value a repeated key carried, in the order sent, as a list β{"tag": ["a", "b"]}β rather than silently keeping only the last one.
A repeated field in an application/x-www-form-urlencoded body (opt=a&opt=b)
gets the same first-value treatment as {{query.param}} for body matchers
and templating.
Match requests based on HTTP headers:
File: mocks/get_protected.json
{
"method": "GET",
"path": "/api/protected",
"status": 200,
"headers": {
"required": {
"authorization": {
"prefix": "Bearer "
}
},
"forbidden": [],
"strict": false
},
"response": {
"data": "This is protected content",
"user": {
"id": 1,
"role": "admin"
}
}
}Usage:
# Matches - valid Bearer token
curl -H "Authorization: Bearer my_token" http://localhost:8080/api/protected
# Doesn't match - missing header
curl http://localhost:8080/api/protected # Returns 404
# Doesn't match - wrong prefix
curl -H "Authorization: Basic token" http://localhost:8080/api/protected # Returns 404Advanced Header Patterns:
{
"headers": {
"required": {
"authorization": "Bearer token123",
"content-type": {"contains": "json"},
"x-api-key": {"regex": "^[A-Za-z0-9]{32}$"},
"accept": {"any": null}
},
"forbidden": ["x-debug", "x-internal"],
"strict": false
}
}- Exact match:
"header": "value" - Prefix match:
"header": {"prefix": "Bearer "} - Contains:
"header": {"contains": "substring"} - Regex match:
"header": {"regex": "^pattern$"} - Any value:
"header": {"any": null} - Forbidden headers: Headers that must NOT be present
- Case-insensitive: Header names are case-insensitive (per HTTP spec)
Match requests based on JSON, text, or form data in the request body:
File: mocks/post_login.json
{
"method": "POST",
"path": "/api/login",
"status": 200,
"headers": {
"required": {
"content-type": "application/json"
}
},
"body": {
"type": "json",
"partial": {
"username": "admin",
"password": "secret123"
}
},
"response": {
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin",
"role": "admin"
}
}
}Usage:
# Matches - exact credentials
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret123"}'
# Matches - extra fields ignored (partial matching)
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret123","remember_me":true}'
# Doesn't match - wrong password
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrong"}' # Returns 404JSON Body Options:
{
"body": {
"type": "json",
"exact": {"key": "value"}, // Entire body must match exactly
"partial": {"name": "Alice"}, // Specified fields must match
"strict": false // If true with partial, reject extra fields
}
}{
"body": {
"type": "text",
"contains": "search term"
}
}Text Body Options:
- Exact:
"exact": "exact string" - Contains:
"contains": "substring" - Regex:
"regex": "^pattern$"
{
"body": {
"type": "form",
"fields": {
"username": "admin",
"password": "secret"
},
"strict": false
}
}Combine all matching types for precise mock selection:
File: mocks/post_search_combined.json
{
"method": "POST",
"path": "/api/search",
"status": 200,
"query_params": {
"params": {
"type": "user"
}
},
"headers": {
"required": {
"authorization": {"prefix": "Bearer "},
"content-type": "application/json"
}
},
"body": {
"type": "json",
"partial": {
"query": "Alice"
}
},
"response": {
"results": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com"
}
],
"total": 1
}
}Usage:
curl -X POST "http://localhost:8080/api/search?type=user" \
-H "Authorization: Bearer my_token" \
-H "Content-Type: application/json" \
-d '{"query":"Alice","filters":{"active":true}}'This mock only matches when all criteria are met:
- β Method is POST
- β
Path is
/api/search - β
Query param
type=user - β Authorization header starts with "Bearer "
- β Content-Type is application/json
- β
Body contains
{"query": "Alice"}
When multiple mocks could match a request, Mimic uses a scoring system:
- Base score: Method + Path match (1000 points)
- Query params: +100 points per matched param
- Headers: +50 points per matched header
- Body: +500 points if body matches
- Path pattern penalty: -100 points for a
:id/{id}match, so an exact path always outranks a pattern
The mock with the highest score wins. Equal scores are broken
deterministically β most literal path segments first (/users/:id beats
/{resource}/:id), then lowest METHOD:path key lexicographically, then
earliest position among mocks sharing that key. The winner never depends on
load order, so it stays the same across restarts and hot reloads. See
ADVANCED_MATCHING.md for details.
Strict header mode ("strict": true) rejects a request that carries a
header the mock didn't declare in required β but a long list of headers
never count as "extra", because a client sends them unconditionally and no
mock is ever written to assert on them:
accept,accept-encoding,accept-language,cache-control,connection,content-length,dnt,host,origin,pragma,referer,upgrade-insecure-requests,user-agent- anything starting with
sec-βsec-fetch-mode,sec-fetch-site,sec-ch-ua,sec-ch-ua-platform, and whatever browsers add to that family next, matched by prefix rather than enumerated by name
That's enough for curl and for a browser's fetch() alike β a strict mock
that passes from curl no longer 404s from the browser it was actually
written to test. A header your client sends that isn't on this list β
MIMIC_STRICT_IGNORE_HEADERS=x-request-id,x-correlation-id (comma-separated,
additive) β extends it without a release. Anything else undeclared, like
x-tenant: acme, is still rejected; strict mode keeps meaning something.
/things and /things/ are the same endpoint as far as most APIs (and most
clients) are concerned, but a mock registered for one used to 404 the other.
Mimic now falls back to the other form automatically, in both directions,
whenever the literal request path matches nothing at all:
# Only /things is registered...
curl http://localhost:8080/things # 200 β the exact match
curl http://localhost:8080/things/ # 200 too β falls back, no redirect-
Exact matches always win. The fallback only ever runs once the literal path β as an exact key and against every pattern route β matched nothing. Register
/thingsand/things/as separate mocks and each serves only its own exact path; neither one falls back to the other. -
Works both directions, and for pattern routes too: a mock at
/users/:idmatches a request to/users/42/just as it matches/users/42. -
The root path (
/) is never touched. There's no other form to fall back to. -
Served directly, with no redirect β a 30x round trip before the real response is exactly what this avoids.
-
The request log stays transparent about it.
GET /admin/requests(and the dashboard's match explanation) always show the path actually requested, and note when the winning mock was only reached by trying its trailing-slash variant:matched mocks/things.json (score 1000: method+path 1000, trailing slash normalized) -
Opt out with
MIMIC_STRICT_TRAILING_SLASH=trueto restore the exact pre-#119 behavior β/thingsand/things/are entirely distinct paths again, no fallback tried either way. -
Applies everywhere path matching happens: body-matcher evaluation, the "does this request need its body read" check, and CORS preflight detection all use the same fallback, so a preflight for
/things/is answered exactly when a realGET /things/would be.
Set arbitrary response headers per mock with response_headers β for CORS, redirects, non-JSON content types, cache control, rate-limit simulation, or auth challenges.
{
"method": "GET",
"path": "/api/data",
"status": 200,
"response_headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS"
},
"response": { "data": [1, 2, 3] }
}Or just set
MIMIC_CORS=trueand skip this entirely β see Built-in CORS below. Per-mock headers are still the way to mock a specific CORS response (including a broken one); the env var is the way to stop repeating them on every endpoint.
{
"method": "GET",
"path": "/data.xml",
"status": 200,
"response_headers": {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "no-cache"
},
"response": "<users><user id=\"1\"/></users>"
}{
"method": "POST",
"path": "/resources",
"status": 201,
"response_headers": {
"Location": "/resources/99",
"X-Request-Id": "abc-123"
},
"response": { "id": 99 }
}- Header names are case-insensitive (
content-typeandContent-Typeboth work). Content-Type: application/jsonis added automatically only when your headers don't set a content type β mocks withoutresponse_headersbehave exactly as before.- When a non-JSON content type is set and
responseis a JSON string, the raw string is sent as the body β so XML/CSV/plain-text responses aren't JSON-quoted. - Invalid header names or values are skipped with a warning; the response is still served.
- Headers apply to every response of the mock, including all sequence steps.
Some bodies don't want to live inside a JSON file. A PDF export can't be
written as JSON at all; a 200 KB captured payload turns a mock into something
nobody can read; a SOAP envelope hand-escaped into a JSON string is impossible
to diff. response_file points a mock at a file next to it and serves that
file's exact bytes.
{
"method": "GET",
"path": "/reports/:id/export",
"status": 200,
"response_file": "fixtures/report.csv",
"response_headers": {
"Content-Disposition": "attachment; filename=\"report.csv\""
}
}curl -i http://localhost:8080/reports/9/export
# HTTP/1.1 200 OK
# content-type: text/csv; charset=utf-8
# content-disposition: attachment; filename="report.csv"
#
# id,name,plan,seats,mrr
# 1,Acme Corp,enterprise,250,12500Binary works the same way, byte for byte:
{
"method": "GET",
"path": "/users/:id/avatar",
"status": 200,
"response_file": "fixtures/avatar.png"
}The path is resolved relative to the mock file's own directory, not to the working directory β so a mocks tree stays relocatable and a Docker volume mount works unchanged:
mocks/
βββ advanced/
βββ get_report_export.json β "response_file": "fixtures/report.csv"
βββ fixtures/
βββ report.csv
βββ invoice.xml
βββ avatar.png
A path that resolves outside the mocks root β ../../etc/passwd, an absolute
path, or a symlink pointing out of the tree β is refused at load time with
an error naming the mock file, and that mock is not registered.
The first of these wins:
Content-Typein the mock's ownresponse_headers;- the file extension:
.json,.xml,.csv,.html,.txt,.png,.jpg/.jpeg,.pdf,.zip; application/octet-stream.
A .json fixture is served as a JSON body, not as a JSON-quoted string.
Set "template": true to interpolate {{path.*}}, {{query.*}},
{{header.*}}, {{body.*}}, and {{faker.*}} inside the file β the same
expressions a response supports:
{
"method": "POST",
"path": "/soap/invoices/:id",
"status": 200,
"template": true,
"response_file": "fixtures/invoice.xml"
}<InvoiceId>{{path.id}}</InvoiceId>
<Currency>{{query.currency}}</Currency>
<CustomerRef>{{body.customer_ref}}</CustomerRef>Templating is off by default and never runs on a binary content type, so a
PNG that happens to contain the bytes {{ is still a PNG.
A sequence step takes the same two fields, so a retry flow can end in a real file:
{
"method": "GET",
"path": "/flaky-export",
"status": 200,
"sequence": [
{ "status": 503, "response": { "error": "unavailable" } },
{ "status": 200, "response_file": "fixtures/report.csv", "repeat": true }
]
}responseandresponse_fileare mutually exclusive. A mock setting both is a load error naming the file. Setting neither is unchanged behavior (response: null).- Files are read at load time and re-read on every hot reload cycle, so editing a fixture takes effect within ~2 s without touching the mock file. Request handling never touches the disk.
MIMIC_MAX_RESPONSE_FILE(default 10 MB) caps one fixture. A file over the cap is reported and its mock is skipped rather than half-loaded;0removes the cap.- A
.jsonfixture is not loaded as a mock. The loader reads every.jsonfile under the mocks directory, but a file some mock claims as itsresponse_fileis served, never registered. - The request log stores a text body verbatim (truncated at
MIMIC_MAX_RECORDED_BODY) and a binary body as a one-line descriptor β<70 bytes of image/png from fixtures/avatar.png>β so the dashboard stays readable. - Everything else composes as usual: path parameters, matchers,
delay_ms, tags, and CORS all work with a file-backed body.
A browser calling a Mimic-backed API from http://localhost:3000 normally fails
twice: it sends OPTIONS /users first (which no mock answers), and every real
mock has to repeat Access-Control-Allow-Origin in its response_headers. For
an API with N endpoints that's up to 2N files maintained by hand.
One variable replaces all of them:
MIMIC_CORS=trueThat's it β preflights are answered automatically and every mock response carries the allow-origin header.
| Variable | Default | What it does |
|---|---|---|
MIMIC_CORS |
false |
Master switch. Everything below is ignored while it's off. |
MIMIC_CORS_ORIGINS |
* |
*, or a comma-separated allowlist like http://localhost:3000,http://localhost:5173. |
MIMIC_CORS_METHODS |
GET,POST,PUT,PATCH,DELETE,OPTIONS |
Advertised as Access-Control-Allow-Methods on a preflight. |
MIMIC_CORS_HEADERS |
* |
* reflects the request's Access-Control-Request-Headers; a list is sent verbatim. |
MIMIC_CORS_CREDENTIALS |
false |
Sends Access-Control-Allow-Credentials: true. |
MIMIC_CORS_MAX_AGE |
600 |
Access-Control-Max-Age, in seconds. |
docker run -d -p 8080:8080 \
-e MIMIC_CORS=true \
-e MIMIC_CORS_ORIGINS=http://localhost:3000,http://localhost:5173 \
-v ./mocks:/app/mocks:ro \
ragilhadi/mimic:latest# No OPTIONS mock anywhere β Mimic answers the preflight itself
curl -i -X OPTIONS http://localhost:8080/users \
-H 'Origin: http://localhost:3000' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-headers: content-type
access-control-max-age: 600- Off by default. With
MIMIC_CORSunset, responses are byte-identical to what they were before this existed β no new headers, andOPTIONSstill 404s. - A mock's own header always wins. If a mock sets
Access-Control-Allow-Originin itsresponse_headers, the global config leaves it alone β so mocking a CORS failure stays possible. - Preflights are gap-filled, not hijacked. An
OPTIONSrequest that no mock matches, but whose path has a mock registered for the method inAccess-Control-Request-Method, is answered204. An explicitOPTIONSmock matches first and wins. A preflight for a path with nothing behind it still404s. - Only real endpoints, in the current scenario. A mock hidden by the active
scenario has no endpoint to preflight, and its
preflight
404s too. - A bare
OPTIONSisn't a preflight. WithoutAccess-Control-Request-Methodβ curl, a health probe β the request behaves exactly as it always has. - Preflights are logged. They appear in
/admin/requestsand the dashboard withmatched_mock: nulland the explanation "answered as a CORS preflight", so nothing looks dropped. - The allowlist is honored. A request from an origin outside
MIMIC_CORS_ORIGINSgets noAccess-Control-Allow-Originheader rather than a wrong one, which is what makes the browser's own error message correct. Responses that depend on the origin also carryVary: Origin. *with credentials.MIMIC_CORS_ORIGINS=*plusMIMIC_CORS_CREDENTIALS=trueis invalid per the CORS spec; Mimic reflects the request origin instead and warns once at startup.- Matching is untouched: CORS headers are added to the response, never to the request the matcher sees.
- Mimic's own error responses carry the headers too. The
mock not found404, thepayload too large413, the admin API's 401, and both proxy fallback outcomes all get the sameAccess-Control-Allow-Origin(and friends) a matched mock would β so a request to a path you forgot to mock reads as a 404 in the browser console, not as an opaque CORS failure. A proxied response that already carries its ownAccess-Control-Allow-Originfrom upstream keeps it.
Simulate slow endpoints to test loading states, timeout handling, retries, circuit breakers, and debounce behavior. Add delay_ms to any mock β the response is held back for that duration before being sent.
{
"method": "GET",
"path": "/slow-endpoint",
"status": 200,
"delay_ms": 2000,
"response": { "data": "finally here" }
}{
"method": "GET",
"path": "/flaky-endpoint",
"status": 200,
"delay_ms": { "min": 100, "max": 3000 },
"response": { "data": "..." }
}Each request samples a fresh uniform value between min and max (inclusive), so repeated calls see realistic variable latency.
- No
delay_msβ zero overhead, responses are as fast as before. - The delay is applied after matching and request recording, with no locks held β a slow mock never blocks other requests, the admin API, or hot reload.
- Works together with sequences: a sequence step's own
delay_mstakes precedence; steps without one inherit the mock-level delay.
A mock can return different responses on successive calls by declaring a sequence array. This makes it possible to test retry logic, rate limiting, flaky services, and multi-step flows without a real server.
{
"method": "POST",
"path": "/api/submit",
"status": 200,
"response": { "ok": true },
"sequence": [
{ "status": 503, "response": { "error": "service unavailable, retry later" } },
{ "status": 429, "response": { "error": "rate limited" }, "delay_ms": 100 },
{ "status": 200, "response": { "ok": true }, "repeat": true }
]
}| Field | Type | Required | Description |
|---|---|---|---|
status |
number | β | HTTP status code for this step |
response |
any JSON | β | Response body for this step |
delay_ms |
number | β | Delay (milliseconds) before returning this step's response |
repeat |
boolean | β | If true, the sequence stops advancing at this step (default false) |
- Steps are consumed in order, one per request.
- A step with
"repeat": trueis returned for all subsequent calls β the sequence stops advancing there. - If no step has
"repeat": true, the last step repeats once the sequence is exhausted. - An empty
sequencearray falls back to the top-levelstatus/response. - Counters are thread-safe and tracked per mock β two mocks sharing a path (differentiated by body/query/header matchers) advance independently.
- Counters survive hot reload of mock files; use the reset endpoint to start over.
With the example above, a client with retry/backoff sees exactly what a recovering service would produce:
curl -X POST http://localhost:8080/api/submit # 503 service unavailable
curl -X POST http://localhost:8080/api/submit # 429 rate limited (after 100ms delay)
curl -X POST http://localhost:8080/api/submit # 200 ok
curl -X POST http://localhost:8080/api/submit # 200 ok (repeats forever)Reset call counters so tests start from step 0 again:
# Reset all sequence counters
curl -X POST http://localhost:8080/admin/sequences/reset
# Reset only the counters for one path
curl -X POST "http://localhost:8080/admin/sequences/reset?path=/api/submit"Response:
{ "reset": 1 }One mocks/ directory can hold both your happy-path mocks and your error
mocks. Tag them, and switch which set is live per CI job or per test run β no
file editing, no second directory, no restart.
{
"method": "POST",
"path": "/checkout",
"status": 500,
"tags": ["error-scenario"],
"response": { "error": "internal error" }
}# mocks/checkout_ok.json β "tags": ["happy-path"]
# mocks/checkout_500.json β "tags": ["error-scenario"]
MIMIC_ACTIVE_TAGS=happy-path mimic # only checkout_ok.json is matchable- A mock with no
tagsis always matchable. Existing mock files need zero changes, and a server started withoutMIMIC_ACTIVE_TAGSbehaves exactly as it did before this feature existed. MIMIC_ACTIVE_TAGSunset or empty means no filtering β every mock, tagged or not, is matchable.- A tagged mock is matchable while at least one of its tags is active.
- Tags are matched exactly and case-sensitively; whitespace around a tag in
the comma-separated list is trimmed (
happy-path, smoke-testis two tags). - An inactive mock 404s as if it were not loaded. Requests fall through to
whatever else can serve the path β an untagged mock, or a
/users/:idpattern route. - Tag filtering does not touch sequence counters: they are keyed per mock, so switching scenarios and back resumes a sequence where it left off rather than restarting it.
- With no filter active, two mocks tagged for opposite scenarios on the
same path are both matchable and one of them wins β so set
MIMIC_ACTIVE_TAGS(orPOST /admin/scenario) whenever you keep competing scenarios side by side.
# What's active right now?
curl http://localhost:8080/admin/scenario{
"active_tags": ["happy-path"],
"filtering": true,
"known_tags": ["error-scenario", "happy-path"],
"matchable_mocks": 12,
"total_mocks": 14
}# Switch scenarios β takes effect on the next request, no restart
curl -X POST http://localhost:8080/admin/scenario \
-d '{"tags": ["error-scenario"]}'
# Turn filtering off again: every mock becomes matchable
curl -X POST http://localhost:8080/admin/scenario -d '{"tags": []}'POST /admin/scenario replaces the active set (it is not additive) and
returns the same body GET does. A tag entry may itself be a comma-separated
list, so {"tags": ["a,b"]} and {"tags": ["a", "b"]} are equivalent. A body
that isn't valid JSON is answered 400 and leaves the current scenario alone.
MIMIC_ACTIVE_TAGS=happy-path mimic &
curl -X POST http://localhost:8080/checkout # 200 {"order_id": "..."}
curl -X POST http://localhost:8080/admin/scenario -d '{"tags": ["error-scenario"]}'
curl -X POST http://localhost:8080/checkout # 500 {"error": "internal error"}/admin/mocksreports each mock'stagsand anactiveflag, so a mock that is loaded but currently filtered out is obvious.- A 404 caused by an inactive mock records "N mock(s) match
POST:/checkoutbut are filtered out by inactive tags" in the request log and the debug log. The 404 response body is unchanged β scenario configuration is never leaked to API clients.
Mock responses don't have to be fully static. Use {{ }} double-brace syntax inside any string value in response (or a sequence step's response) to echo back data from the incoming request β no custom code required.
| Template | Source |
|---|---|
{{query.page}} |
URL query parameter ?page=2 |
{{header.x-request-id}} |
Request header value (case-insensitive) |
{{body.username}} |
Top-level JSON (or form) body field |
{{body.user.email}} |
Nested JSON body field (dot notation) |
{{body.items.0.sku}} or {{body.items[0].sku}} |
Array element by index β either syntax |
{{path.id}} |
Named path parameter :id or {id} |
{{faker.uuid}} |
Random generated data β no request input needed |
Credential headers are never echoed.
{{header.authorization}},{{header.cookie}}and{{header.set-cookie}}always render as an empty string β the same as a missing key β regardless of letter case. This matches the redaction already applied to the/admin/requestslog, so a mock file can't reflect a live bearer token or session cookie into a response body (and from there into browser devtools, HAR exports, or CI logs).
{
"method": "POST",
"path": "/users",
"status": 201,
"response": {
"id": 99,
"username": "{{body.username}}",
"email": "{{body.email}}",
"created_by": "{{header.x-actor}}",
"self_url": "/users/99"
}
}curl -X POST http://localhost:8080/users \
-H "X-Actor: admin" \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"alice@example.com"}'Response:
{
"id": 99,
"username": "alice",
"email": "alice@example.com",
"created_by": "admin",
"self_url": "/users/99"
}{{body.β¦}} templates can index into arrays as well as objects β a zero-based
integer segment reads an array element, in either dot or bracket syntax:
{
"method": "POST",
"path": "/orders",
"status": 201,
"response": {
"first_sku": "{{body.items.0.sku}}",
"first_sku_bracket": "{{body.items[0].sku}}",
"first_qty": "{{number:body.items[0].qty}}"
}
}curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"items":[{"sku":"WIDGET-1","qty":3},{"sku":"WIDGET-2","qty":1}]}'Response:
{
"first_sku": "WIDGET-1",
"first_sku_bracket": "WIDGET-1",
"first_qty": 3
}Notes:
- Dot and bracket syntax are interchangeable, at every level.
body.items.0.sku,body.items[0].sku, and evenbody.a.0.b.1(nested arrays, mixed with object fields) all resolve the same way β pick whichever reads better for a given field. - Object keys win over array indices. A JSON object whose keys happen to
look numeric (
{"0": "zero"}) still resolves by key lookup first; only an actual array is indexed by position. - Out-of-range and non-numeric indices degrade gracefully β an empty
string (or
nullunder ajson:cast), never a panic β the same as any other missing key. - Every typed cast works on an indexed path:
{{number:body.items.0.qty}},{{json:body.items.0}}, and so on.
The faker source doesn't read the request at all β it generates a fresh, plausible-looking value on every call, so one mock file can stand in for a whole fixture set.
| Template | Output |
|---|---|
{{faker.uuid}} |
RFC 4122 v4 UUID, e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6 |
{{faker.int}} |
Random integer in the default range 0..=1000000 |
{{faker.int min=1 max=100}} |
Random integer in [1, 100] |
{{faker.bool}} |
true or false |
{{faker.name}} |
Random name, e.g. Priya Novak |
{{faker.email}} |
Slugified random name at example.com, e.g. priya.novak@example.com |
{{faker.timestamp}} |
Current UTC time in RFC 3339, e.g. 2026-07-22T16:12:54.481+00:00 |
{
"method": "GET",
"path": "/faker/user",
"status": 200,
"response": {
"id": "{{faker.uuid}}",
"name": "{{faker.name}}",
"email": "{{faker.email}}",
"age": "{{faker.int min=18 max=99}}",
"verified": "{{faker.bool}}",
"created_at": "{{faker.timestamp}}"
}
}curl http://localhost:8080/faker/userResponse (a different one every call):
{
"id": "9c0f1b6d-2f4e-4a1e-b0a2-6c1d7f3e88a4",
"name": "Priya Novak",
"email": "priya.novak@example.com",
"age": "37",
"verified": "true",
"created_at": "2026-07-22T16:12:54.481+00:00"
}Notes:
- Every occurrence resolves independently β two
{{faker.uuid}}in one response produce two different UUIDs, and{{faker.email}}is not derived from a{{faker.name}}sitting next to it. - Faker values are always rendered as JSON strings, since templates are interpolated into string values.
- Malformed arguments degrade to the generator's defaults instead of failing:
{{faker.int min=abc}}and{{faker.int min=100 max=1}}both use the default0..=1000000range. - An unknown generator (e.g.
{{faker.credit_card}}) renders as an empty string, like any other unknown template.
By default every templated value is spliced into a JSON string, even when
the source is a number or boolean β {"id": "{{path.id}}"} on GET /users/42
renders {"id": "42"}, not {"id": 42}. That's a hard failure for a typed
client (zod, Go's encoding/json, Rust's serde_json, Jackson, β¦), which
rejects a string where a number or object belongs. Prefix any source
expression with number:, bool:, or json: to render it as that JSON type
instead:
| Template | Renders as |
|---|---|
{{number:path.id}} |
JSON number |
{{bool:query.active}} |
JSON boolean (true/false) |
{{json:body.user}} |
JSON value parsed from the source (object, array, number, β¦) |
{
"id": "{{number:path.id}}",
"views": "{{number:faker.int min=1 max=999}}",
"active": "{{bool:query.active}}",
"user": "{{json:body.user}}"
}renders {"id": 42, "views": 317, "active": true, "user": {"name": "alice"}}
instead of every field coming back quoted.
Notes:
- Whole-string only. A cast applies only when the string is exactly one
{{cast:source.key}}expression."user-{{number:path.id}}"stays plain string interpolation β a number can't be spliced into the middle of a string and still be a number β and renders"user-42". - Failed casts degrade, never panic.
{{number:path.slug}}on/users/abcfalls back to the plain string"abc", and{{json:query.x}}on a value that isn't valid JSON falls back the same way. - Null handling differs by cast.
{{json:body.missing}}on a field that wasn't sent renders JSONnull;{{number:...}}and{{bool:...}}fall back to an empty string on a missing key, same as an unprefixed template. - An unrecognized cast word (anything other than
number,bool, orjson) resolves to an empty string, the same as an unrecognized source. - Works everywhere templates work β
response, sequence step responses, and with every source (path,query,header,body,faker) β and never affects matching, since templates (casts included) are resolved after a mock is already chosen.
- Templates are resolved after the mock/sequence step is chosen, so the interpolated value never affects matching itself β
{{faker.*}}included, so a faker expression sitting in a matcher is treated as a literal string. - An unknown source, a missing key, or an explicit JSON
nullall resolve to an empty string β malformed templates never panic and never leak into the response. - Non-string body values (numbers, booleans, nested objects/arrays) render using their JSON text form, e.g.
{{body.age}}for{"age": 30}produces30. - A response with no
{{ }}expressions is returned unchanged with no templating overhead. - See Typed Casts above for opt-in
{{number:x}}/{{bool:x}}/{{json:x}}output.
Already have an OpenAPI 3.x spec? Generate a whole mocks/ directory from it in one command instead of hand-writing every file:
mimic import-openapi ./spec.yaml --out ./mocks/generatedJSON and YAML specs are both accepted as input. The importer writes one live mock file per operation β built from its primary response β plus an inert .json.disabled file for every other documented status (.yaml.disabled with --format yaml). The output is a plain MockConfig, JSON by default; pass --format yaml to write .yaml files instead β either way it hot-reloads like any other mock and can be hand-edited afterwards.
The default output directory, ./mocks/generated, sits inside the ./mocks
the server falls back to locally β so mimic import-openapi ./spec.yaml && mimic
serves the generated mocks with nothing else to configure. When --out points
elsewhere, the importer prints the exact command to start the server against
it.
| Option | Default | Description |
|---|---|---|
--out <dir> |
./mocks/generated |
Output directory |
--status <code> |
200 |
Status treated as each operation's primary response; its file is the live one |
--format <json|yaml> |
json |
Output format for generated mock files (yml is accepted as an alias for yaml) |
--force |
off | Write into a non-empty output directory, overwriting files of the same name |
--brace-params |
off | Emit path parameters as {id} instead of :id |
-h, --help |
β | Show usage |
Given this spec:
openapi: 3.0.3
info: { title: Petstore, version: 1.0.0 }
paths:
/users/{id}:
get:
responses:
'200':
content:
application/json:
example: { "id": 1, "name": "Alice" }
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
properties:
code: { type: integer }
message: { type: string }mimic import-openapi spec.yaml --out ./mocks/generated writes:
mocks/generated/get_users_id.json (the primary 200 response β no status suffix)
{
"method": "GET",
"path": "/users/:id",
"status": 200,
"response": { "id": 1, "name": "Alice" },
"consume_body": false
}mocks/generated/get_users_id_404.json.disabled (an alternative response β inert until renamed)
{
"method": "GET",
"path": "/users/:id",
"status": 404,
"response": { "code": 0, "message": "string" },
"consume_body": false
}mimic import-openapi spec.yaml --out ./mocks/generated --format yaml writes the same two mocks as get_users_id.yaml and get_users_id_404.yaml.disabled instead:
method: GET
path: /users/:id
status: 200
response:
id: 1
name: Alice
consume_body: falseStart the server and GET /users/42 returns the 200 body immediately β path parameters are translated to Mimic's :id syntax, so generated mocks work with the matcher unedited.
To serve the 404 instead, swap which file is live:
mv mocks/generated/get_users_id.json mocks/generated/get_users_id_200.json.disabled
mv mocks/generated/get_users_id_404.json.disabled mocks/generated/get_users_id_404.jsonHot reload picks the change up within a couple of seconds β no restart needed.
For each response, the importer takes the first of these that the spec provides:
content.<media type>.example- The first entry in
content.<media type>.examples(itsvalue) - A stub built from
content.<media type>.schema {}β for body-less responses like204
JSON media types are preferred when a response offers several.
When there's no example, the schema is walked recursively and each field gets a type-appropriate placeholder:
| Schema | Stub |
|---|---|
type: string |
"string" (or a format-appropriate value: date, date-time, uuid, email, uri) |
type: integer / number |
0 / 0.0 |
type: boolean |
false |
type: object |
{} with every property stubbed recursively |
type: array |
one stubbed element from items, or [] if items is absent |
enum / default / example |
the spec's own value, which always wins over a stub |
allOf subschemas are merged; oneOf / anyOf use the first variant.
- Multiple response codes become separate files, and only the primary one is live. Alternatives share a path and method with the primary response and carry no matcher to tell them apart, so leaving them all enabled would make the served response depend on directory read order. They land as
.json.disabledinstead β rename to enable. They are alternative responses, not a call-order sequence, so they are also deliberately not folded into a singlesequencemock. - The primary response is
--statusif the operation declares it, otherwise its lowest declared2xx, otherwise its lowest declared status. So aPOSTdocumenting only201gets a live201, and an operation documenting only errors still gets a live mock rather than an all-disabled route. - OpenAPI's
defaultkey maps to--statusfor its filename, but never outranks a declared success response β it usually documents an error shape, and serving that by default would be misleading. It becomes the live mock only when it's all an operation has. $refis resolved for schemas, responses, path items, and named examples. Circular references collapse to{}at the point of recursion rather than looping forever; external (./other.yaml#/...) and unresolvable refs degrade to{}with a warning.- Existing files are protected. A non-empty
--outdirectory is refused unless you pass--force, so a re-import can't silently clobber mocks you edited by hand. With--force, every overwritten file is named in a warning. - Response keys may be quoted (
'200') or bare (200),default, or wildcards (4XXβ400). - Only OpenAPI 3.x is supported. Swagger 2.0 specs are rejected with a clear message β convert first.
- A malformed spec produces an error and a non-zero exit code, never a panic.
Point Mimic at a real API once, and it grows a matching mock set for free. When MIMIC_PROXY_UPSTREAM is set, a request that matches no local mock is forwarded to that upstream instead of getting the usual 404 "mock not found" β the real response is returned to the client. Add MIMIC_RECORD_UPSTREAM=true and that response is also saved as a new mock file, so the next identical request is served from disk with zero network calls.
MIMIC_PROXY_UPSTREAM=https://api.stripe.com MIMIC_RECORD_UPSTREAM=true mimic# First call: no local mock exists, so Mimic forwards to api.stripe.com
# and returns the live response.
curl http://localhost:8080/v1/charges/ch_123
# A new file appears at mocks/_recorded/get_v1_charges_ch_123_1.json.
# The second identical call is served from that file β Stripe never sees it.
curl http://localhost:8080/v1/charges/ch_123MIMIC_PROXY_UPSTREAM=https://api.example.com # unset (default) = unchanged 404 behavior
MIMIC_RECORD_UPSTREAM=false # opt-in recording of proxied responses
MIMIC_PROXY_TIMEOUT_MS=5000 # how long to wait for the upstream
MIMIC_RECORD_MATCH_HEADERS= # extra request headers to pin as matchers (comma-separated)A recorded file uses the exact same shape as any hand-written mock β method, path, status, response, plus matchers built from the request that triggered the recording, so the next matching request is matched normally rather than through special-cased "recorded" logic:
query_paramsβ every query parameter, as an exact match.headersβ an allowlist, not everything the client sent: onlycontent-typeby default, plus whateverMIMIC_RECORD_MATCH_HEADERSnames (comma-separated, e.g.x-tenant,accept). Everything else β trace and correlation ids (x-request-id,traceparent), browser headers (origin,referer,accept-language,sec-fetch-*,sec-ch-ua*), and any other header the client happened to send β is left out, and sensitive headers (Authorization,Cookie) never make it in regardless of what's configured. This is why: most of those headers vary on every request from a real client, and pinning them asrequiredused to mean a recording matched only the one request that created it β see #105. Widen the list only for an API that genuinely varies its response by a header your workflow needs to distinguish.bodyβ a JSON, text, or form matcher depending on the request's content type; no matcher at all for an empty body.response_headersβ the upstream's response headers, withSet-Cookieand friends left out, anddate/serverdropped too β a replay must not re-serve the timestamp of the original exchange.
Files land at mocks/_recorded/<method>_<sanitized-path>_<n>.json β e.g. mocks/_recorded/get_v1_charges_ch_123_1.json β fully readable and editable like any other mock. They pick up on the next hot reload, typically within a couple of seconds.
- Recording is best-effort and never blocks the response. The client gets the upstream's response immediately; the mock file is written in the background.
- Only text-ish responses are recorded. JSON, XML, plain text, JavaScript, and form-encoded bodies are recorded; binary responses (images, PDFs, arbitrary
application/octet-stream) are still proxied to the client but never turned into a mock file β there's no good text representation for aresponsefield. - Concurrent identical requests dedupe onto one file. Several requests with the same method, path, query, (non-noise) headers, and body in flight at once produce exactly one recording, not a race of several.
- A repeat of the same request doesn't re-record. Once a given request shape has been recorded this run, later identical proxy calls (before the file has been hot-reloaded in, or if recording is on but nothing changed) are skipped rather than rewritten.
- What Mimic reserves for itself is never proxied. The health check and the admin API's own endpoints β see Reserved endpoints β are excluded even with no local mock behind them, so
MIMIC_PROXY_UPSTREAMcan't leak them onto the upstream. A path that merely starts with/admin/but isn't one Mimic answers (a typo, or an endpoint you're mocking) is an ordinary request and proxies like any other. - Self-referential upstreams are rejected at startup. An upstream that resolves to Mimic's own listening address (e.g.
MIMIC_PROXY_UPSTREAM=http://localhost:8080while Mimic itself listens on8080) disables proxying with a warning, instead of looping forever. - A slow or unreachable upstream falls back to the usual 404, with an added
"upstream_error"field explaining why β never an indefinitely hanging request. The wait is capped byMIMIC_PROXY_TIMEOUT_MS. - A gzip/deflate/brotli-compressed upstream response is decoded before it's forwarded or recorded (#106) β Mimic's HTTP client decompresses these automatically, so both the live response the client gets and the recorded mock's
responseare plain text/JSON, never the raw compressed bytes a previous version could silently mangle into mojibake. This costs a modestly larger binary (three extra decompression codecs statically linked in); worth it for a proxy whose whole job is to be transparent about what an upstream actually said. An encoding this build doesn't recognize (anything outside gzip/deflate/brotli) is left exactly as the upstream sent it β forwarded to the client unchanged, and skipped from recording (with awarn!) rather than written as a corrupt mock.
http://localhost:8080/admin/dashboard
A single dependency-free page β no build step, no CDN β for answering the two
questions that otherwise send you back to RUST_LOG=debug: what is this
server configured to do? and why didn't my mock match?
| Tab | What it shows |
|---|---|
| Requests | Every recorded request. Expand a row for its headers, query params and body, the response that was actually served (status, headers, body), and a Match section explaining which mock won and why β or, for a 404, which mocks were in the running and what rejected each. |
| Mocks | Every loaded mock: method, path, status, which matchers it declares, delay, sequence length, hit count, and the file it came from. Expand a row for the full MockConfig JSON. |
| Sequences | Each stateful sequence's current step, with a per-path Reset button. |
The header bar reads /health for mocks loaded, uptime, port, and max body size.
A matched request records the arithmetic behind its score:
matched mocks/get_users_id.json (score 1150: method+path 1000, headers +50, path pattern -100)
An unmatched one records the near-miss diagnosis instead β the mocks that
shared its METHOD:path, and the first matcher that turned each down:
2 candidate mock(s) for `GET:/users`, none matched:
mocks/get_users.json β required header `x-api-key` was absent;
mocks/get_users_admin.json β query param `role` was `viewer`, expected `admin`
When nothing is registered at all, the explanation says so β and points out the wrong-verb case, which is what it usually is:
no mock is registered for `GET:/users` β `/users` is registered for POST, PUT
Explanations are produced by the matchers themselves (each match_* predicate
is defined as "no rejection reason"), so an explanation can never describe a
rule the server doesn't actually enforce.
| Filter | Behaviour |
|---|---|
| Path | Substring match (user finds /users and /users/active) |
| Method | Exact, case-insensitive |
| Status | An exact code (404) or a class (4xx, 5xx) |
| Unmatched only | Keeps just the requests no mock served |
| Search | Case-insensitive, over each request's body, headers and query params |
All of them are query parameters on /admin/requests, so they work from
curl too β the dashboard is a client of the same public API.
- Auto-refresh appends new rows rather than rebuilding the table, and pauses while the pointer is over it β new requests collect behind a "N new requests" banner instead of shifting the row you're reading.
- Paginated, not everything at once (#111): the table loads the 50 most recent matches, with a Load older requests button underneath to fetch further back. The "Shown" stat reads the total matching the current filters, even though only a page of rows is actually rendered β the dashboard itself is what used to have to re-fetch and re-render the whole log on every refresh tick.
- Copy as curl per row, and Export log for the whole filtered view β regardless of how much of it is currently paged in.
- Theme follows
prefers-color-scheme, with a manual toggle that sticks. - Redaction: see What the request log keeps β headers and body fields are scrubbed by default.
- Bounded by default: the log keeps the last
MIMIC_MAX_LOG_ENTRIESrequests (1000), and stored bodies are truncated pastMIMIC_MAX_RECORDED_BODY(64 KB) with aβ¦[truncated]marker β so a long-running server doesn't degrade the very UI meant to observe it.
The log lives in memory and is served, unfiltered, by GET /admin/requests on
a server that binds 0.0.0.0 by default. Two of this README's own use cases β
a shared team mock server, and CI β put that port somewhere more than one
person can reach it (restrict it with MIMIC_BIND_ADDRESS
if that's not what you want). So credentials that pass through Mimic are
scrubbed on the way in to the log.
Redacted by default:
| Where | What | Configured by |
|---|---|---|
| Request headers, response headers, match explanations | authorization, cookie, set-cookie |
β (fixed list) |
| Request bodies and response bodies | fields named password, passwd, token, access_token, refresh_token, id_token, secret, client_secret, api_key, apikey, private_key, authorization |
MIMIC_REDACT_BODY_FIELDS |
Body redaction walks JSON β through nested objects and arrays β and
application/x-www-form-urlencoded fields. Field names match
case-insensitively and exactly: token scrubs token, not tokenizer,
which is why the default list spells out the common variants. A matching key
loses its whole value, object or array included. Anything with no field
structure (plain text, XML, binary) is stored as it came in.
Not redacted: query strings, paths, and the values of fields you haven't
named. ?api_key=... in a URL is stored verbatim β put secrets in headers or
bodies.
Binary response bodies β a response_file
with a non-text content type β are stored as a descriptor
(<70 bytes of image/png from fixtures/avatar.png>) rather than as bytes, so
they never blow past MIMIC_MAX_RECORDED_BODY or arrive in the dashboard as a
wall of replacement characters.
Redaction is a property of the log only. The body sent to the client, the
body matching runs against, and the values {{body.*}} interpolates are all
untouched.
MIMIC_REDACT_BODY_FIELDS=password,cvv,pin mimic # replace the default list
MIMIC_REDACT_BODY_FIELDS= mimic # store bodies verbatim
MIMIC_DISABLE_BODY_LOG=true mimic # store no bodies at allMIMIC_ADMIN_TOKEN puts the admin endpoints behind a bearer token. Unset β
the default β leaves them open exactly as they have always been.
MIMIC_ADMIN_TOKEN=s3cret mimiccurl -H 'Authorization: Bearer s3cret' http://localhost:8080/admin/requestsWithout the header, those endpoints answer 401 {"error": "unauthorized"}.
Two things stay open on purpose: /health, because liveness probes call it
and carry no credentials, and any path under the admin prefix that Mimic
doesn't itself answer (GET /admin/users), because that's an ordinary mock.
Note that /admin/dashboard is guarded too, so a browser won't load it while a
token is set β use the token with curl, move the admin API somewhere private
with MIMIC_ADMIN_PREFIX, or leave the token unset on a machine only you can
reach.
Mock backend APIs while building your frontend:
# Start Mimic with your API mocks, with CORS on for your dev server
docker run -d -p 8080:8080 \
-e MIMIC_CORS=true \
-e MIMIC_CORS_ORIGINS=http://localhost:3000 \
-v ./mocks:/app/mocks:ro ragilhadi/mimic:latest
# Point your frontend to http://localhost:8080 β preflights are handled for youQuickly prototype API responses:
# Create mock files
echo '{"method":"GET","path":"/api/v1/products","status":200,"response":[]}' > mocks/products.json
# Start Mimic
docker compose up -dSimulate external APIs for testing:
# Mock Stripe API
# Mock GitHub API
# Mock any REST API- Rust 1.70+ (for building from source)
- Docker (for containerized development)
- Make (optional, for convenience commands)
- Clone the repository:
git clone https://github.com/ragilhadi/mimic.git
cd mimic- Run locally:
# Using Makefile
make dev
# Or using Cargo directly
cargo runEither one serves the mocks in this repo's ./mocks directory β no
configuration needed. To read a different directory, set MIMIC_MOCKS_DIR:
MIMIC_MOCKS_DIR=./fixtures/staging cargo run- Run tests:
# Run all tests
make test
# Run with verbose output
make test-verbose
# Generate coverage report
make test-coverage# Development
make dev # Run development server with debug logging
make watch # Auto-rebuild and run on file changes
make check # Quick compile check
# Building
make build # Build debug binary
make release # Build optimized release binary
# Testing
make test # Run all tests
make test-verbose # Run tests with output
make test-coverage# Generate test coverage report
make ci-local # Run all CI checks locally
# Code Quality
make fmt # Format code with rustfmt
make lint # Run clippy linter
make audit # Security audit of dependencies
# Docker
make docker-build # Build Docker image
make docker-run # Run in Docker container
make docker-compose-up # Start with docker-compose
make docker-compose-down# Stop docker-compose services
# Cleanup
make clean # Remove build artifactsMimic uses GitHub Actions for automated testing and deployment:
-
Unit Tests (
.github/workflows/unit-test.yml)- Runs on every push to
main - Runs on every pull request
- Checks code formatting
- Runs linter (clippy)
- Executes all 35+ unit tests
- Tests on multiple Rust versions (stable, beta, nightly)
- Performs security audit
- Generates coverage report
- Runs on every push to
-
Docker Build & Push (
.github/workflows/docker-build-push.yml)- Builds Docker image on push to
main - Pushes to Docker Hub automatically
- Multi-platform support (amd64, arm64)
- Version tagging from
vars/versionfile
- Builds Docker image on push to
- β 35+ Unit Tests - Comprehensive test coverage
- β ~90% Code Coverage - High quality assurance
- β Zero Clippy Warnings - Clean, idiomatic Rust code
- β Security Audited - Dependencies checked for vulnerabilities
Pre-built images are available on Docker Hub:
Repository: ragilhadi/mimic
# Latest version
docker pull ragilhadi/mimic:latest
# Specific version
docker pull ragilhadi/mimic:v1.0.0- Base Image: Debian Bookworm Slim
- Size: ~90 MB (compressed)
- Platforms: linux/amd64, linux/arm64
- Runtime: Minimal dependencies (ca-certificates, wget)
Mimic includes a built-in health check endpoint:
curl http://localhost:8080/healthResponse:
{
"status": "healthy",
"mocks_loaded": 5,
"mock_count": 7,
"service": "mimic",
"version": "1.14.0",
"uptime_seconds": 4021,
"port": 8080,
"max_body_size": 10485760,
"max_log_entries": 1000,
"max_recorded_body": 65536,
"requests_recorded": 138
}mocks_loaded counts registered METHOD:path routes; mock_count counts mock
definitions, which is larger when several mocks share one route. The admin
dashboard reads this endpoint for its header summary bar.
Use this endpoint for:
- Docker health checks
- Kubernetes liveness/readiness probes
- Load balancer health checks
- Monitoring systems
Mimic supports configurable request body consumption per endpoint via the consume_body field.
By default, consume_body is false (fast performance):
- Mimic responds immediately without reading the request body
- Optimal for endpoints that don't need the body
- Best performance and lowest memory usage
The decision is made per endpoint, scoped to the mocks registered for the
request's method and path β a body matcher on some unrelated mock elsewhere
in your mocks directory has no effect on this one.
Mimic reads the body when any mock that could serve the request:
- sets
"consume_body": true, or - declares a
bodymatcher (it can't match what it hasn't read), or - interpolates
{{body.β¦}}into itsresponseor a sequence step's response
...or when no mock is registered at all for that method and path, so the 404 response and the request log can still show what the client sent.
Otherwise the body is left unread β which is exactly what consume_body: false
promises.
Set consume_body: true for endpoints that handle:
-
File Uploads
{ "method": "POST", "path": "/upload-document", "status": 200, "consume_body": true, "response": {"uploaded": true} } -
Multipart/Form-Data
{ "method": "POST", "path": "/ocr-image", "status": 200, "consume_body": true, "response": {"text": "extracted text"} } -
Large Payloads
- Prevents "Broken Pipe" errors
- Required when clients send large request bodies
- Critical for image/document processing endpoints
| consume_body | Speed | Memory | Use Case |
|---|---|---|---|
false (default) |
β‘ Fastest | Minimal | No body expected |
true |
Standard | Temporary spike | File uploads, large payloads |
Example:
# Works with consume_body: true
curl -X POST http://localhost:8080/ocr-image \
-F "image=@large-document.jpg"
# Works with consume_body: false (default)
curl -X POST http://localhost:8080/trigger-jobRequest bodies are capped at 10 MB by default. The cap is enforced while the body streams in, so an oversized request is turned away before Mimic allocates memory for it β a client cannot drive the server's memory use past the limit no matter how much it sends.
Over-limit requests get a 413 Payload Too Large:
{
"error": "payload too large",
"method": "POST",
"path": "/upload",
"max_body_size": 10485760
}Raise or lower the cap with the MIMIC_MAX_BODY_SIZE environment variable
(in bytes):
MIMIC_MAX_BODY_SIZE=52428800 mimic # 50 MBAn unset, unparsable, or zero value falls back to the 10 MB default. The active limit is printed at startup.
GET /health
Returns server status, loaded mock counts, and the runtime configuration the dashboard's summary bar displays. See Health Check.
GET /admin/dashboard # Web dashboard (Requests / Mocks / Sequences)
GET /admin/requests # List recorded requests (see filters below)
DELETE /admin/requests # Clear recorded requests
GET /admin/mocks # List every loaded mock, with matchers and hit counts
GET /admin/sequences # Current step of every in-progress sequence
POST /admin/sequences/reset # Reset sequence counters (optional: ?path=/api/submit)
GET /admin/scenario # Which scenario tags are currently active
POST /admin/scenario # Replace the active tag set (body: {"tags": [...]})
All admin endpoints are read-only apart from the ones that say otherwise, and all return JSON β the dashboard is just one client of them.
| Query param | Meaning |
|---|---|
path |
Substring of the request path |
method |
Exact method, case-insensitive |
status |
Exact code (404) or class (4xx, 5xx) |
unmatched_only |
true/1/yes β only requests no mock served |
search |
Case-insensitive text search over body, headers and query params |
limit |
How many matches to return, applied after every filter above. Defaults to 50; limit=0 returns every match, the pre-#111 behavior |
offset |
How many of the most recent matches to skip before taking limit β offset=0 (the default) is the newest page |
# Every 4xx whose path mentions "user" and that nothing matched
curl "http://localhost:8080/admin/requests?path=user&status=4xx&unmatched_only=true"
# The 50 requests immediately before the most recent 50
curl "http://localhost:8080/admin/requests?limit=50&offset=50"Each record carries the request as before, plus β when there is something to report β the response served and the match diagnosis:
{
"id": 12,
"timestamp": "2026-08-07T10:15:04Z",
"method": "GET",
"path": "/users/42",
"query_params": { "fields": ["id", "name"] },
"headers": { "accept": "application/json", "authorization": "[REDACTED]" },
"matched_mock": "GET:/users/42",
"response_status": 200,
"response_body": "{\"id\":42}",
"response_headers": { "content-type": "application/json" },
"match_score": 900,
"path_params": { "id": "42" },
"match_explanation": "matched mocks/get_users_id.json (score 900: method+path 1000, path pattern -100)"
}Every field after response_status is additive and omitted when empty, so
existing consumers of this endpoint keep working unchanged.
query_params is one exception (#108): each value is now a list of
every occurrence that key had in the request, in order β ?fields=id&fields=name
is {"fields": ["id", "name"]}, and a key sent once is still a one-element
list, {"page": ["1"]}. Previously a repeated key silently collapsed to its
last value and the field was a plain string per key; a request log written by
that older Mimic still loads, with each value wrapped into a one-element list.
Pagination (#111). The top level of the response carries a page of
requests plus enough to know there's more:
{
"count": 1000,
"returned": 50,
"limit": 50,
"offset": 0,
"requests": [ /* the 50 most recent matches */ ]
}count keeps meaning exactly what it always has β the total number of
requests matching the filters β so a consumer that reads it for "how many
matched" sees the same number as before. requests is now a page of that
total rather than always all of it: the previous version returned every
match in one response, which meant a busy server's /admin/requests could
approach tens of megabytes and the dashboard re-fetched all of it every few
seconds. returned is requests.length, for a consumer that wants it
without counting. Ask for limit=0 to get the old everything-in-one-response
behavior back.
{
"count": 12,
"mocks": [
{
"key": "GET:/users/:id",
"index": 0,
"method": "GET",
"path": "/users/:id",
"status": 200,
"source": "mocks/generated/get_users_id.json",
"has_path_params": true,
"matchers": { "query_params": false, "headers": true, "body": false },
"delay_ms": null,
"sequence_steps": null,
"response_headers": 1,
"consume_body": false,
"hits": 4,
"tags": [],
"active": true,
"config": { "method": "GET", "path": "/users/:id", "status": 200, "response": {} }
}
]
}source is the file the mock was loaded from, and is absent for mocks not read
from disk. config is the full MockConfig. hits counts requests served by
that specific mock, so two mocks sharing a path stay distinguishable. The
endpoint reads through the same lock hot reload writes to, so it always reflects
the mock set currently serving. tags and active describe the mock's
scenario membership β active: false means
the mock is loaded but filtered out by the current scenario, and so unmatchable.
{
"count": 1,
"sequences": [
{
"key": "POST:/submit#0",
"method": "POST",
"path": "/submit",
"step": 2,
"total": 3,
"source": "mocks/post_submit.json"
}
]
}step is how many calls the sequence has served β the index of the step the
next request will get. A sequence appears only once it has served a request.
Returns the number of counters that were reset:
{ "reset": 2 }{
"active_tags": ["happy-path"],
"filtering": true,
"known_tags": ["error-scenario", "happy-path"],
"matchable_mocks": 12,
"total_mocks": 14
}known_tags is every tag declared by a loaded mock. filtering is false
(and active_tags empty) when no scenario filter is configured, i.e. every
mock is matchable.
Replaces the active tag set and returns the same body as the GET:
curl -X POST http://localhost:8080/admin/scenario -d '{"tags": ["error-scenario"]}'The body is read as JSON regardless of Content-Type, so plain curl -d works.
{"tags": []} clears the filter; a malformed body is a 400 and leaves the
current scenario untouched. See
Tagged Mock Groups.
All other endpoints are defined by your mock files. Mimic will:
- Match the HTTP method and path
- Return the configured status code
- Return the configured response body
If no mock matches β 404 Not Found. The body echoes what the server
actually received, so you can see why nothing matched:
{
"error": "mock not found",
"method": "GET",
"path": "/undefined",
"query_params": {},
"headers_received": ["host", "user-agent", "accept"]
}query_params is the parsed query string and headers_received lists the
header names the request arrived with (names only β values are never echoed).
Both fields are always present. See
ADVANCED_MATCHING.md for using this to debug
a mismatch.
If the request body exceeds the size limit β 413 Payload Too Large. See
Maximum Body Size:
{
"error": "payload too large",
"method": "POST",
"path": "/upload",
"max_body_size": 10485760
}Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
make test - Run linter:
make lint - Format code:
make fmt - Submit a pull request
# Run all tests
make test
# Run with coverage
make test-coverage
# Run all CI checks
make ci-localThis project is licensed under the MIT License - see the LICENSE file for details.
Built with:
- Rust - Systems programming language
- Axum - Web framework
- Tokio - Async runtime
- Serde - Serialization framework
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Docker Hub: ragilhadi/mimic
- Request body matching
- Query parameter matching
- Header matching
- Stateful response sequences (different response per call)
- Response delays (mock-level
delay_ms, fixed or random range, plus per sequence step) - Custom response headers (
response_headersβ CORS, redirects, non-JSON content types) - Admin UI (request history dashboard)
- Hot reload for mock files
- Dynamic response templating (
{{query.x}},{{header.x}},{{body.x}},{{path.x}}) - Path parameter matching (
:id,{id}syntax) - Faker-style random data generators (
{{faker.uuid}},{{faker.name}},{{faker.int}}, β¦) - Typed template casts (
{{number:x}},{{bool:x}},{{json:x}}) - Built-in CORS with automatic
OPTIONSpreflight handling (MIMIC_CORS=true)
Made with β€οΈ using Rust