Every frontend developer has been there. The backend team says the API will be ready “next week.” Next week arrives, and it’s “almost done, just a few more things.” Meanwhile, your components are stubbed with hardcoded data, integration is blocked, and you’re building in the dark.
mockr is a CLI tool I built to fix this. It lets frontend teams define mock REST and gRPC APIs from a config file, serve them locally, and seamlessly proxy anything that isn’t mocked yet to the real backend. You move at your own pace. The backend catches up.
What is mockr?
mockr is a zero-dependency CLI written in Go that runs a local HTTP and gRPC server. You define routes in a TOML, YAML, or JSON config file, and mockr serves stub responses from those files. When a route isn’t mocked yet, it forwards the request to a real upstream API.
Key properties that make it useful for a team workflow:
- Hot reload — edit a config file and the next request picks up the change. No restart, no disruption.
- Route-based cases — each route can have multiple named response cases (success, error, empty). Switch between them by changing a single
fallbackfield. - Generate from a spec — bootstrap a complete mock config from any OpenAPI 3 spec or
.protofile in one command. - Proxy fallthrough — unmatched routes forward to the real backend, so you only mock what you’re actively building.
- Stateful mocks — POST/PUT/PATCH/DELETE can mutate stub files on disk. Subsequent GETs reflect the change.
Install
Download a binary from the latest release:
# macOS Apple Silicon
curl -L https://github.com/ridakaddir/mockr/releases/latest/download/mockr_darwin_arm64.tar.gz | tar xz
sudo mv mockr /usr/local/bin/
# macOS Intel
curl -L https://github.com/ridakaddir/mockr/releases/latest/download/mockr_darwin_amd64.tar.gz | tar xz
sudo mv mockr /usr/local/bin/
# Linux x86-64
curl -L https://github.com/ridakaddir/mockr/releases/latest/download/mockr_linux_amd64.tar.gz | tar xz
sudo mv mockr /usr/local/bin/
Or install with Go:
go install github.com/ridakaddir/mockr@latest
Bootstrap from an OpenAPI Spec
If your team has already agreed on an API contract — even a draft one — you can generate a full mock config in one command.
# From a local file
mockr generate --spec openapi.yaml --out ./mocks
# From a remote URL
mockr generate --spec https://petstore3.swagger.io/api/v3/openapi.json --out ./mocks
mockr reads every path and operation in the spec and generates:
- One config file per tag (e.g.
users.toml,orders.toml) - One stub JSON file per response status code in
stubs/ - Stubs populated with realistic placeholder values based on field names and schema formats
The output directory looks like this:
mocks/
├── users.toml
├── orders.toml
└── stubs/
├── get_users_200.json
├── post_users_201.json
├── get_users_id_200.json
├── get_users_id_404.json
└── ...
A generated users.toml looks like:
# Generated from openapi.yaml
# Tag: users
[[routes]]
method = "GET"
match = "/users"
enabled = true
fallback = "success"
[routes.cases.success]
status = 200
file = "stubs/get_users_200.json"
[routes.cases.empty]
status = 200
file = "stubs/get_users_empty_200.json"
[[routes]]
method = "GET"
match = "/users/*"
enabled = true
fallback = "success"
[routes.cases.success]
status = 200
file = "stubs/get_users_id_200.json"
[routes.cases.not_found]
status = 404
file = "stubs/get_users_id_404.json"
Start serving immediately — no editing required:
mockr --config ./mocks
Your frontend now has a fully functional mock API at http://localhost:4000. The team can start building against it while the backend is still being implemented.
Mocking REST APIs: The Config in Depth
Named cases and fallback
Every route has a cases map and a fallback pointing to one of those cases. To simulate an error state, change fallback = "not_found" and save. Hot reload picks it up instantly.
[[routes]]
method = "GET"
match = "/users/*"
enabled = true
fallback = "success" # change this to "not_found" to test error handling
[routes.cases.success]
status = 200
file = "stubs/user.json"
[routes.cases.not_found]
status = 404
json = '{"error": "user not found"}'
[routes.cases.server_error]
status = 500
json = '{"error": "internal server error"}'
delay = 2
The delay field (in seconds) lets you simulate slow endpoints — useful for testing loading states and skeleton screens.
Conditions: route by request content
Conditions let mockr serve different cases based on what’s in the request — body fields, query params, or headers. They’re evaluated top-to-bottom; the first match wins.
[[routes]]
method = "POST"
match = "/orders"
enabled = true
fallback = "created"
[[routes.conditions]]
source = "body"
field = "payment_type"
op = "eq"
value = "crypto"
case = "pending_review"
[[routes.conditions]]
source = "header"
field = "X-User-Role"
op = "eq"
value = "guest"
case = "forbidden"
[[routes.conditions]]
source = "query"
field = "dry_run"
op = "exists"
case = "dry_run_response"
[routes.cases.created]
status = 201
json = '{"id": "{{uuid}}", "status": "confirmed"}'
[routes.cases.pending_review]
status = 202
json = '{"id": "{{uuid}}", "status": "pending_review"}'
[routes.cases.forbidden]
status = 403
json = '{"error": "guests cannot place orders"}'
[routes.cases.dry_run_response]
status = 200
json = '{"valid": true, "estimated_total": 99.99}'
Supported operators: eq, neq, contains, regex, exists, not_exists.
Template tokens
Use {{uuid}}, {{now}}, and {{timestamp}} in inline JSON values. They are rendered fresh on every request.
[routes.cases.created]
status = 201
json = '{"id": "{{uuid}}", "created_at": "{{now}}", "ts": {{timestamp}}}'
Stateful mocks: full CRUD without a real backend
By adding persist: true to a case, mutating requests update the stub file on disk. Subsequent reads reflect the change — giving you a fully functional in-memory-style CRUD API.
# GET — read from stub file
[[routes]]
method = "GET"
match = "/users"
enabled = true
fallback = "success"
[routes.cases.success]
status = 200
file = "stubs/users.json"
# POST — append the request body as a new record
[[routes]]
method = "POST"
match = "/users"
enabled = true
fallback = "created"
[routes.cases.created]
status = 201
file = "stubs/users.json"
persist = true
merge = "append"
array_key = "users"
# PUT — replace the record matching id
[[routes]]
method = "PUT"
match = "/users/*"
enabled = true
fallback = "updated"
[routes.cases.updated]
status = 200
file = "stubs/users.json"
persist = true
merge = "replace"
key = "id"
array_key = "users"
# DELETE — remove the record matching id
[[routes]]
method = "DELETE"
match = "/users/*"
enabled = true
fallback = "deleted"
[routes.cases.deleted]
status = 204
file = "stubs/users.json"
persist = true
merge = "delete"
key = "id"
array_key = "users"
The stubs/users.json stub acts as your local database. Create, update, and delete operations modify it in place.
Dynamic file resolution
If your stub files are per-resource (e.g. one JSON file per user), use {source.field} placeholders in the file path. mockr resolves them from the request at runtime.
[routes.cases.success]
status = 200
file = "stubs/user-{query.id}-profile.json"
A request to GET /profile?id=42 resolves to stubs/user-42-profile.json. If the file doesn’t exist, mockr falls through to the next condition or fallback.
Response transitions: simulate async workflows
Some features depend on status that changes over time — order fulfillment, payment processing, file uploads. Response transitions let you model this without writing any code.
[[routes]]
method = "GET"
match = "/orders/*"
enabled = true
fallback = "processing"
[[routes.transitions]]
case = "processing"
after = 10 # seconds from first request → advance
[[routes.transitions]]
case = "shipped"
after = 60
[[routes.transitions]]
case = "delivered"
# no after — terminal state
[routes.cases.processing]
status = 200
json = '{"status": "processing"}'
[routes.cases.shipped]
status = 200
json = '{"status": "shipped"}'
[routes.cases.delivered]
status = 200
json = '{"status": "delivered"}'
The first request returns processing. After 10 seconds, it returns shipped. After 60 seconds, delivered — permanently. Your frontend’s polling logic, status badges, and toast notifications can all be tested against realistic async behavior.
Proxy Fallthrough: Mock What You’re Building, Forward the Rest
The --target flag turns mockr into a selective reverse proxy. Routes defined in your config return mock responses. Everything else is forwarded to the real upstream.
mockr --config ./mocks --target https://api.example.com
This is the core workflow for a frontend team working ahead of the backend:
- Backend exposes a staging API with some endpoints already done
- Frontend mocks only the endpoints still in progress
- Everything else proxies transparently to staging
Each response in the terminal logs tells you how it was served:
GET /users/1 200 via=stub 1ms
GET /products 200 via=proxy 84ms
POST /orders 201 via=stub 0ms
When the backend ships an endpoint, remove its route from your config. The next request falls through to the real API automatically.
Use --api-prefix when your frontend calls /api/* but the real upstream uses bare paths:
mockr --config ./mocks \
--target https://api.example.com \
--api-prefix /api
Record Mode: Capture the Real API First
If you have access to a real API — staging, a third-party service, or even production — record mode lets you capture its responses as stub files in one pass.
mockr --config ./mocks \
--target https://api.example.com \
--api-prefix /api \
--record
Browse your app normally. mockr proxies every request, saves each response as a stub file, and appends a route entry to mocks/recorded.toml. The recorded latency is stored as the delay field so replays feel realistic.
Request 1 → via=proxy 73ms (saved to stubs/get_users_1.json)
Request 2 → via=stub <1ms (served from recorded stub)
Once you’ve captured enough responses, drop --record and --target and serve fully offline:
mockr --config ./mocks --api-prefix /api
This is useful for locking in known-good responses for demo environments, CI test runs, or working on planes.
gRPC: Same Workflow for gRPC Services
mockr supports gRPC alongside HTTP — both servers run in the same process. No protoc, no code generation, no plugins required.
Generate config from a .proto file
mockr generate --proto service.proto --out ./mocks
For a UserService with GetUser, ListUsers, and CreateUser, mockr generates:
mocks/
├── mockr.toml # [[grpc_routes]] for all methods
└── stubs/
├── UserService_GetUser.json
├── UserService_ListUsers.json
└── UserService_CreateUser.json
Stubs are synthesised from the output message descriptor — fields named *_id get "{{uuid}}", fields named *_at get "{{now}}", and so on.
Start the server — HTTP on port 4000 and gRPC on port 50051:
mockr --config ./mocks --grpc-proto service.proto
Define gRPC routes manually
[[grpc_routes]] live in the same config files as [[routes]]. The match field is the full gRPC method path.
[[grpc_routes]]
match = "/users.UserService/GetUser"
enabled = true
fallback = "ok"
# Return not_found for user_id = "999"
[[grpc_routes.conditions]]
source = "body"
field = "user_id"
op = "eq"
value = "999"
case = "not_found"
[grpc_routes.cases.ok]
status = 0 # gRPC OK
file = "stubs/UserService_GetUser.json"
[grpc_routes.cases.not_found]
status = 5 # gRPC NOT_FOUND
json = '{"message": "user not found"}'
[grpc_routes.cases.internal]
status = 13 # gRPC INTERNAL
json = '{"message": "internal server error"}'
delay = 1
Conditions on gRPC request body fields work identically to REST — both snake_case and camelCase field names are accepted automatically.
Use grpcurl and grpc-ui without a .proto file
mockr registers gRPC server reflection automatically. This means grpcurl and grpc-ui can discover your services and call them without needing the .proto file locally.
# List all services
grpcurl -plaintext localhost:50051 list
# Describe a service
grpcurl -plaintext localhost:50051 describe users.UserService
# Call a method
grpcurl -plaintext \
-d '{"user_id": "1"}' \
localhost:50051 users.UserService/GetUser
Proxy unmatched gRPC methods
Like HTTP, unmatched gRPC methods forward to an upstream server via --grpc-target:
mockr --config ./mocks \
--grpc-proto service.proto \
--grpc-target localhost:9090
You can mock individual methods while forwarding everything else to a real gRPC backend — the same selective approach as HTTP.
Hot Reload
mockr watches the config file or directory for changes using fsnotify. Edit a case, change a fallback, or drop a new .toml file into the config directory — the next request picks it up. No restart, no lost state in the browser.
This makes it practical to use mockr as a shared fixture server during a sprint. One developer manages the config; everyone on the frontend team points their dev server proxy at the same mockr instance.
The Team Workflow
Here is how this plays out in practice:
- Sprint kickoff — agree on the API contract with the backend team. Document it as an OpenAPI spec or
.protofile. - Generate mocks —
mockr generate --spec openapi.yaml --out ./mocksormockr generate --proto service.proto --out ./mocks. - Frontend starts immediately — point
NEXT_PUBLIC_API_URL(or your equivalent) athttp://localhost:4000and build. - Iterate on stubs — as designs evolve, edit stub JSON files or add new cases. Hot reload keeps everything live.
- Backend ships endpoints — as each endpoint is ready on staging, remove its route from the config. Requests fall through to the real API.
- Integration is not a big bang — because you’ve been working against realistic stubs from day one, integration is a series of small, expected verifications rather than a chaotic end-of-sprint scramble.
Conclusion
Waiting for the backend is a coordination problem as much as a technical one. mockr gives frontend teams the tools to break that dependency: a config-driven mock server that starts from your existing API contracts, adapts as requirements change, and transparently hands off to the real backend as it comes online.
The result is that frontend development becomes a first-class, independently shippable activity — not a process that stalls when the backend is behind.
More info and source code: github.com/ridakaddir/mockr