back
mockr - How Frontend Teams Work Ahead of the Backend
Mar 21, 2026·12 min read

mockr - How Frontend Teams Work Ahead of the Backend


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:

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:

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:

  1. Backend exposes a staging API with some endpoints already done
  2. Frontend mocks only the endpoints still in progress
  3. 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:

  1. Sprint kickoff — agree on the API contract with the backend team. Document it as an OpenAPI spec or .proto file.
  2. Generate mocksmockr generate --spec openapi.yaml --out ./mocks or mockr generate --proto service.proto --out ./mocks.
  3. Frontend starts immediately — point NEXT_PUBLIC_API_URL (or your equivalent) at http://localhost:4000 and build.
  4. Iterate on stubs — as designs evolve, edit stub JSON files or add new cases. Hot reload keeps everything live.
  5. Backend ships endpoints — as each endpoint is ready on staging, remove its route from the config. Requests fall through to the real API.
  6. 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