---
title: "Test and Mock GraphQL APIs Without Trusting the 200 | Speedscale"
description: "Generate GraphQL tests and mocks from a schema with proxymock, catch failures hidden inside HTTP 200 responses, and flag schema changes before clients break."
canonical: "https://speedscale.com/blog/graphql-mocks-tests-contract-checks/"
---

![Test and Mock GraphQL APIs Without Trusting the 200](https://speedscale.com/_astro/featured-image.DP5pOGlH_2wEP01.webp)

# Test and Mock GraphQL APIs Without Trusting the 200

Oct 26, 2026

[By Matt LeRay](https://speedscale.com/blog/author/matt-leray/)

[API testing](https://speedscale.com/blog/category/api-testing/) [Developer tools](https://speedscale.com/blog/category/developer-tools/) [Microservices](https://speedscale.com/blog/category/microservices/)

* * *

Replay a GraphQL recording at the server that produced it, and every request comes back `200 OK`. Here is one of those 200s, from a `Session` query whose recorded token has expired:

```
{
  "data": { "session": null },
  "errors": [
    { "message": "token expired", "locations": [{ "line": 2, "column": 3 }], "path": ["session"] }
  ]
}
```

A GraphQL server reports most failures inside a successful HTTP response, so any check that stops at the status code passes this request. And that is the second way GraphQL hides from URL-shaped tooling. The first: every operation is a `POST` to the same `/graphql` endpoint, so a coverage report, a mock server or a dashboard keyed on method and path sees one endpoint, called eight times, all green.

This post walks through testing a GraphQL API with [proxymock](https://speedscale.com/proxymock/) so that neither trick works. You will generate tests and mocks from the schema of a running server, record its real responses, replay them as a regression test that catches the error above, mock the API for its client, measure what the recording covers per operation, and flag a schema change in CI. It is the GraphQL companion to [gRPC Mocks, Tests and Contract Checks From a .proto](https://speedscale.com/blog/test-grpc-service-from-proto/). Everything below ran on a real service written in Java, Node.js, Go and Python, with every output copied from those runs, and proxymock never needed to know which language it was talking to.

🎯 Key Takeaways

*   proxymock reads each GraphQL request as the operation it runs, so replay results, mocks and coverage are per operation, not per `/graphql`.
*   A GraphQL error inside a 200 fails a replay as a body mismatch, and a semantic-path transform on `variables.token` fixes an expired token for good.
*   `proxymock validate` resolves each recorded request’s selection against the schema, so a type change is caught even where a union fragment hides it.

```
flowchart LR
    S[Running server] -->|introspection| G[proxymock generate]
    G --> T[Tests and mocks]
    S --> R[proxymock record]
    R --> P[proxymock replay]
    R --> M[proxymock mock]
    R --> C[proxymock coverage]
    R --> V[proxymock validate in CI]
```

## What you need

*   A proxymock build with GraphQL schema support. This feature is awaiting a public release; the walkthrough currently requires a build from [speedscale!7235](https://gitlab.com/speedscale/speedscale/-/merge_requests/7235). `proxymock generate --help` must mention GraphQL before you continue. Follow the [installation and activation instructions](https://docs.speedscale.com/proxymock/getting-started/installation/) before recording.
*   Java 17 or later with Maven, Node.js 20 or later, Go 1.23 or later, or Python 3.10 or later, whichever you work in. Step 6 also uses `jq`.
*   The example: [graphql-demo](https://github.com/speedscale/demo/tree/master/graphql-demo) from Speedscale’s open source demo repository. It is a small SaaS API with four queries, two mutations, a union, and a driver that exercises aliases, named and inline fragments, a document holding two operations, and a `Session` query that verifies a signed JWT that expires after five minutes.

The demo comes in four editions, each built on its language’s standard GraphQL library: [graphql-java](https://www.graphql-java.com/), [graphql-js](https://github.com/graphql/graphql-js), [graphql-go](https://github.com/graphql-go/graphql) and [graphql-core](https://github.com/graphql-python/graphql-core). They define the same schema, answer the same way, and their drivers send byte-for-byte the same requests, so every output in this post is the same whichever one you pick. Where they differ, the post says so. Pick a language here and every code block below follows it:

**Java**

```
git clone https://github.com/speedscale/demo && cd demo/graphql-demo/java
mvn -q package
java -jar target/graphql-demo.jar    # serves POST /graphql on :8080
```

**Node.js**

```
git clone https://github.com/speedscale/demo && cd demo/graphql-demo/node
npm ci
node graphql-demo.js    # serves POST /graphql on :8080
```

**Go**

```
git clone https://github.com/speedscale/demo && cd demo/graphql-demo
go build -o graphql-demo .
./graphql-demo    # serves POST /graphql on :8080
```

**Python**

```
git clone https://github.com/speedscale/demo && cd demo/graphql-demo/python
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python graphql_demo.py    # serves POST /graphql on :8080
```

## Step 1: Generate tests and mocks from the schema

Every edition defines its schema in code, as code-first servers do, so there is no `.graphql` file to point at. Here is the `User` type and the `createUser` mutation in each:

**Java**

```
GraphQLObjectType userType = GraphQLObjectType.newObject().name("User")
        .field(newFieldDefinition().name("id").type(nonNull(GraphQLString)))
        .field(newFieldDefinition().name("email").type(nonNull(GraphQLString)))
        .field(newFieldDefinition().name("plan").type(GraphQLString))
        .field(newFieldDefinition().name("tags").type(list(GraphQLString)))
        .build();

GraphQLObjectType mutationType = GraphQLObjectType.newObject().name("Mutation")
        .field(newFieldDefinition().name("createUser").type(userType)
                .argument(newArgument().name("input").type(nonNull(createUserInput))))
        // trackEvent ...
        .build();
```

**Node.js**

```
const userType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: new GraphQLNonNull(GraphQLString) },
    email: { type: new GraphQLNonNull(GraphQLString) },
    plan: { type: GraphQLString },
    tags: { type: new GraphQLList(GraphQLString) },
  },
})

const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    createUser: {
      type: userType,
      args: { input: { type: new GraphQLNonNull(createUserInput) } },
      resolve: (_, { input }) => ({ /* ... */ }),
    },
    // trackEvent ...
  },
})
```

**Go**

```
userType := graphql.NewObject(graphql.ObjectConfig{
	Name: "User",
	Fields: graphql.Fields{
		"id":    &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
		"email": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
		"plan":  &graphql.Field{Type: graphql.String},
		"tags":  &graphql.Field{Type: graphql.NewList(graphql.String)},
	},
})

mutationType := graphql.NewObject(graphql.ObjectConfig{
	Name: "Mutation",
	Fields: graphql.Fields{
		"createUser": &graphql.Field{
			Type: userType,
			Args: graphql.FieldConfigArgument{
				"input": &graphql.ArgumentConfig{Type: graphql.NewNonNull(createUserInput)},
			},
			Resolve: func(p graphql.ResolveParams) (any, error) { /* ... */ },
		},
		// trackEvent ...
	},
})
```

**Python**

```
user_type = GraphQLObjectType("User", {
    "id": GraphQLField(GraphQLNonNull(GraphQLString)),
    "email": GraphQLField(GraphQLNonNull(GraphQLString)),
    "plan": GraphQLField(GraphQLString),
    "tags": GraphQLField(GraphQLList(GraphQLString)),
})

mutation_type = GraphQLObjectType("Mutation", {
    "createUser": GraphQLField(
        user_type,
        args={"input": GraphQLArgument(GraphQLNonNull(create_user_input))},
        resolve=resolve_create_user,
    ),
    # "trackEvent": ...
})
```

`proxymock generate` takes the running endpoint instead and introspects it, so the language the schema is written in never comes up. An SDL file or a saved introspection result works the same way.

```
proxymock generate --direction both --out ./proxymock/generated http://localhost:8080/graphql
```

```
Processing 6 operations from GraphQL schema...
...
PLACEHOLDERS: 2 of 6 inbound test(s) still send generated placeholders ("example_value" or ${{param:...}}).
Generated inbound tests are drivers, not a baseline: replace the placeholders with real values
before replaying them at a real implementation or recording them through proxymock record.
  - proxymock/generated/localhost/query_search_200_inbound.md
  - proxymock/generated/localhost/mutation_trackEvent_200_inbound.md
```

Each root query and mutation field becomes one named operation, written once as an inbound test and once as an outbound mock. Here is what the `createUser` test sends:

```
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    email
    plan
    tags
  }
}
```

with `{"input": {"email": "user@example.com"}}` as its variables. The selection follows the order the schema lists fields in, so against the Go edition, whose library lists them alphabetically, `email` comes first. The selection takes every scalar field one level deep, required arguments become variables, input objects get their required fields, and values come from the field names: `id` gets `12345`, `email` an address. A union like `search` selects `__typename` and an inline fragment per member. Subscriptions are listed as skipped, and `--include-paths 'query.*'` narrows a large schema to the operations you want.

The two placeholders are honest. `search(q: "example_value")` returns a made-up user and no plans, and `trackEvent` takes a JSON document serialized into a string, which `example_value` is not. Generated tests are drivers for a first recording, not a baseline.

## Step 2: Record the real responses

The demo ships its own client, so record that instead of the generated drivers. Stop the server from step 1 and start it again under the recorder:

**Java**

```
proxymock record --app-port 8080 -- java -jar target/graphql-demo.jar
```

**Node.js**

```
proxymock record --app-port 8080 -- node graphql-demo.js
```

**Go**

```
proxymock record --app-port 8080 -- ./graphql-demo
```

**Python**

```
proxymock record --app-port 8080 -- .venv/bin/python graphql_demo.py
```

Then, in a second terminal, send the driver through proxymock’s inbound port, 4143:

**Java**

```
java -jar target/graphql-demo.jar -drive -target http://localhost:4143
```

**Node.js**

```
node graphql-demo.js -drive -target http://localhost:4143
```

**Go**

```
./graphql-demo -drive -target http://localhost:4143
```

**Python**

```
.venv/bin/python graphql_demo.py -drive -target http://localhost:4143
```

```
2026/10/26 17:19:54 Session    ok
2026/10/26 17:19:54 CreateUser ok
2026/10/26 17:19:54 Dashboard  ok
2026/10/26 17:19:54 Find       ok
2026/10/26 17:19:54 GetPlans   ok
2026/10/26 17:19:54 GetUser    ok
2026/10/26 17:19:54 Track      ok
2026/10/26 17:19:54 APQ        ok (expected error: PersistedQueryNotFound)
2026/10/26 17:19:54 RESTSearch ok
```

Stop the recorder with Ctrl-C. Keep the recording directory created in this step: steps 3, 5 and 6 use this inbound recording, while step 4 creates a separate outbound recording. Each request is a file in `proxymock/recorded-<timestamp>/localhost/`, and the request body is stored as the parsed document rather than as query text. That is what lets proxymock match, edit and report on a request by what it means. A persisted query, which carries a hash and no query, is kept exactly as it was sent.

## Step 3: Replay it as a regression test

Start the server on its own again, with the command from [What you need](https://speedscale.com/blog/graphql-mocks-tests-contract-checks/#what-you-need), and replay the recording at it:

```
proxymock replay --in proxymock/recorded-<timestamp> --test-against http://localhost:8080
```

```
│ ENDPOINT             │ METHOD      │ COUNT │ ... │ FAILED │ STATUS CODE MISMATCH │
│ /(.*)/search         │ POST        │     1 │ ... │      0 │                   0% │
│ /graphql             │ POST        │     1 │ ... │      0 │                   0% │
│ CreateUser           │ mutation    │     1 │ ... │      0 │                   0% │
│ Dashboard,UserFields │ query       │     1 │ ... │      0 │                   0% │
│ Find                 │ query       │     1 │ ... │      0 │                   0% │
│ GetPlans,GetUser     │ query,query │     2 │ ... │      0 │                   0% │
│ Track                │ mutation    │     1 │ ... │      0 │                   0% │
│ Session              │ query       │     1 │ ... │      0 │                   0% │
```

The report is per operation, not per URL. `Dashboard,UserFields` is an operation that spreads a named fragment. `GetPlans,GetUser` is one document holding two operations, recorded twice with a different `operationName` each time. The lone `/graphql` row is the persisted query, which has no document to read, and `/(.*)/search` is the REST endpoint the driver calls as a negative case. Within five minutes of recording, the verdict is `pass`.

Replay the same recording once the token’s five minutes are up, and the table looks just as healthy: `Session` still shows 0 failed and 0% status mismatch. The verdict does not:

```
Replay verdict: proxymock/results/replayed-<timestamp>/replay-verdict.json (mismatch)
```

```
sequenceDiagram
    participant Replay as proxymock replay
    participant Server as graphql-demo
    Replay->>Server: query Session (recorded token)
    Server-->>Replay: 200, data.session null, errors: token expired
    Note over Replay: status 200 = 200, so the table shows nothing
    Note over Replay: body differs from the recording, so the verdict is mismatch
```

`replay-verdict.json` has one failing entry: `/graphql`, `POST`, recorded status 200, observed status 200, status `pass`, body `fail`. It is the `token expired` response from the top of this post. A status check calls this request healthy, and the body comparison catches it. The replay command still exits 0 by default; for a CI gate, check the verdict explicitly:

```
jq -e '.verdict == "pass"' proxymock/results/replayed-<timestamp>/replay-verdict.json
```

This exits 1 for the expired-token mismatch and 0 for a passing replay.

The fix belongs in the test, not the recording. The token rides in `variables.token`, and proxymock’s [`graphql` transform](https://docs.speedscale.com/guides/transformation/transforms/graphql/) reaches it by that path, a [semantic path](https://docs.speedscale.com/guides/graphql/semantic-paths/) that survives reordered arguments and reformatted queries, where a JSON path into the parsed document would not. Save a blueprint that re-signs it before each replay. The demo’s HS256 key is published for exactly this:

```
{
  "id": "6f1c2a9e-3b7d-4e2a-9c51-2d8f0b7a4e10",
  "name": "Resign session token",
  "active": true,
  "tokenizeConfig": {
    "name": "Resign session token",
    "id": "6f1c2a9e-3b7d-4e2a-9c51-2d8f0b7a4e10",
    "generator": [
      {
        "filters": { "filters": [{ "include": true, "optUrl": "/graphql" }] },
        "extractor": { "type": "req_body" },
        "transforms": [
          { "type": "graphql", "config": { "path": "variables.token" } },
          { "type": "jwt_resign", "config": { "key": "speedscale-demo-secret" } }
        ]
      }
    ]
  }
}
```

Put it in `proxymock/blueprints/resign-session-token.json`. The `generator` chain rewrites what replay sends. It is scoped by URL path because, once proxymock reads a request as GraphQL, its endpoint is the operation name, not `/graphql`. [`jwt_resign`](https://docs.speedscale.com/guides/transformation/transforms/jwt_resign/) keeps the token’s claims and algorithm and moves its expiry forward. For simpler fixes you do not need to write JSON: proxymock web’s GraphQL panel puts a transform button on every variable and argument, to replace a value, store it for a later request, or smart-replace it.

```
proxymock replay --in proxymock/recorded-<timestamp> --test-against http://localhost:8080 \
  --require-blueprint 'Resign session token'
```

```
Blueprint "Resign session token": 1 transform chain(s) ran.
Replay verdict: proxymock/results/replayed-<timestamp>/replay-verdict.json (pass)
```

The server logs `graphql op="Session" ok`, in every edition. `--require-blueprint` fails the run if the chain never fires, so a renamed variable cannot quietly turn this test back into a status check.

## Step 4: Mock the API for its client

Now flip the roles: the driver is a client, and the GraphQL API is the dependency it calls. Record the driver’s calls through a mapped port while the server runs:

**Java**

```
proxymock record --map 8081=http://localhost:8080 -- \
  java -jar target/graphql-demo.jar -drive -target http://localhost:8081
```

**Node.js**

```
proxymock record --map 8081=http://localhost:8080 -- \
  node graphql-demo.js -drive -target http://localhost:8081
```

**Go**

```
proxymock record --map 8081=http://localhost:8080 -- \
  ./graphql-demo -drive -target http://localhost:8081
```

**Python**

```
proxymock record --map 8081=http://localhost:8080 -- \
  .venv/bin/python graphql_demo.py -drive -target http://localhost:8081
```

The recorder stops when the driver exits. Stop the server and start the mock:

**Java**

```
proxymock mock --in proxymock/recorded-<timestamp> --map 8081=http://localhost:8080 --no-passthrough -- \
  java -jar target/graphql-demo.jar -drive -target http://localhost:8081
```

**Node.js**

```
proxymock mock --in proxymock/recorded-<timestamp> --map 8081=http://localhost:8080 --no-passthrough -- \
  node graphql-demo.js -drive -target http://localhost:8081
```

**Go**

```
proxymock mock --in proxymock/recorded-<timestamp> --map 8081=http://localhost:8080 --no-passthrough -- \
  ./graphql-demo -drive -target http://localhost:8081
```

**Python**

```
proxymock mock --in proxymock/recorded-<timestamp> --map 8081=http://localhost:8080 --no-passthrough -- \
  .venv/bin/python graphql_demo.py -drive -target http://localhost:8081
```

The mapped port matters because Go’s and Java’s HTTP clients never send `localhost` through a proxy, so the port is how proxymock gets in between. Node.js and Python’s `urllib` do use the proxy proxymock sets up for them, so the same commands work there too, and the recording simply holds each call twice, once at each port. With the server stopped, the driver fails on its first request:

**Java**

```
2026/10/26 19:32:02 pass 1: Session: decoding response: No content to map due to end-of-input
```

**Node.js**

```
2026/10/26 19:31:27 pass 1: Session: decoding response: Unexpected end of JSON input
```

**Go**

```
2026/10/26 19:31:03 pass 1: Session: decoding response: EOF
```

**Python**

```
2026/10/26 19:31:44 pass 1: Session: decoding response: Expecting value: line 1 column 1 (char 0)
```

A GraphQL mock matches on the parsed document and its variables. Whitespace and argument order do not matter. Selection order does, because it is the order of the fields in the response. And every run of this driver signs a new JWT and draws a new request id, so its `Session` query never matches the one recorded.

proxymock’s mock recommender finds the fields that drifted between the request and its nearest recording:

```
1. POST /graphql — service responder, 1 request(s)
   - id: responder|body:variables.requestId
     fix: http.req.bodyBase64.variables.requestId -> constant
   - id: responder|body:variables.token
     fix: http.req.bodyBase64.variables.token -> constant
```

It recommends masking `variables.token` and `variables.requestId`, and nothing else. It never masks the query or the operation name, because those say which operation a request is; masking them would let any operation match any other. Accept both with **Apply all recommended** in proxymock web’s Mocks view, or ask your coding assistant to run the proxymock MCP server’s `mocks` tool, and run the driver again. It gets past `Session` and stops at `CreateUser`, whose email address is generated per run. One more accepted fix, and the third run completes:

```
2026/10/26 19:31:05 pass 1 complete: 7 GraphQL operations + APQ + REST negative
```

All nine calls are answered from the recording, with no server running, in all four languages. The fixes live in a blueprint in the workspace, so the next run, a teammate’s run and CI all get them.

## Step 5: See what the recording covers

The server was stopped in step 4. Start it again with the server command from [What you need](https://speedscale.com/blog/graphql-mocks-tests-contract-checks/#what-you-need) so proxymock can introspect its schema. Use the **inbound recording from step 2** for `--in` here and in step 6, rather than the outbound recording from step 4.

```
proxymock coverage --spec http://localhost:8080/graphql --in proxymock/recorded-<timestamp>
```

```
cached the introspection result at proxymock/recorded-2026-10-26_21-19-54.625402Z/localhost_8080.introspection.json
Recording/schema coverage: 9 inbound HTTP pair(s), 7 matched
Covered: 6/6 operations, 6/12 statuses, 23/27 fields
POST query.user: 2 pair(s)
  status 200: 2 pair(s)
    $.data.user: 2 pair(s)
    $.data.user.email: 2 pair(s)
    $.data.user.id: 2 pair(s)
    $.data.user.plan: 1 pair(s)
    $.data.user.tags: 0 pair(s)
  status 200+errors: 0 pair(s)
...
Unknown routes: 2
  POST /graphql (200): persisted query without query text
  POST /api/search (200): not a GraphQL request
Ranked gaps:
  1. status POST mutation.createUser (200+errors)
  2. status POST mutation.trackEvent (200+errors)
  ...
```

By URL, this recording is one endpoint, fully covered. By operation, every query and mutation was reached, but the statuses line tells the real story: `200+errors`, a 200 whose body carries errors, was never recorded for any of the six. The recording proves the happy path and nothing about how the API fails. Those gaps rank first, because they are what the next recording should add.

Fields are counted under their schema names, so the driver’s `me: user(...)` alias covers `$.data.user`, and fields selected through fragments count like any other. The two unknown routes are the persisted query and the REST negative case, listed rather than silently dropped. The `--json` flag prints JSON to standard output. Redirect it to keep a report beside the recording and compare it over time:

```
proxymock coverage --spec http://localhost:8080/graphql --in proxymock/recorded-<timestamp> --json \
  > proxymock/recorded-<timestamp>/coverage.json
```

## Step 6: Catch the schema change

A GraphQL server validates its own responses against its own schema, so how can a response ever disagree with a schema? When the schema you test against is not the one the server was built from: the `schema.graphql` a frontend team codes against, the next version in a pull request, or a dependency you do not own.

Say someone changes `Plan.price` from `Int!` to `String!`, to carry a currency, while the deployed server still sends integers. Make that change in a copy of the introspection result:

```
jq '(.data.__schema.types[] | select(.name == "Plan") | .fields[] | select(.name == "price") | .type.ofType.name) = "String"' \
  proxymock/recorded-<timestamp>/localhost_8080.introspection.json > schema-v2.json
proxymock validate --spec ./schema-v2.json --in proxymock/recorded-<timestamp>
```

Against the live schema the same recording is `7 conformant` and exits 0. Against the changed one:

```
VIOLATION  POST query.search (200)  [...]
  $.data.search[1].price: type mismatch, expected String, got integer (49)
VIOLATION  POST query.plans (200)  [...]
  $.data.plans[0].price: type mismatch, expected String, got integer (0)
  $.data.plans[1].price: type mismatch, expected String, got integer (49)
  $.data.plans[2].price: type mismatch, expected String, got integer (499)

checked 7 pair(s): 5 conformant, 2 violating, 0 without a spec route
```

The command exits 2. Look at the first violation. Nobody asked `search` for a `price` field directly: it came from an inline fragment, `... on Plan { name price }`, inside a union. A search of the client code for `plans` would miss it. `validate` finds it because it does not read responses as loose JSON. It parses each recorded request’s document, resolves the selection against the schema, aliases and fragments included, and walks the response alongside it. Along the way it also flags `null` in a non-null field, a value outside its enum, a selected field missing from the response, and a field nobody selected.

The exit codes are made for CI: 0 conformant, 2 violations, 3 an operation whose root field is not in the schema, and 1 when the check cannot run. Point `--spec` at a URL and it uses the cached introspection when the server is down, so the check still runs on a build agent.

## What does not work yet

*   **Subscriptions.** `generate` lists them as skipped, and a recorded subscription is not checked as one.
*   **Requests that are not a JSON POST.** A query in the URL, a batched array of operations, and an `application/graphql` body are recorded and replayed as sent, but not read as operations.
*   **Other paths.** proxymock recognizes GraphQL by a path ending in `/graphql`. An API served elsewhere still records, mocks and replays as JSON over HTTP, without the per-operation view.
*   **Generated mocks answer one document.** A mock generated from the schema matches the generated selection and variables. A client that asks for different fields gets no match. Record the client, or edit the generated request to match.

## From an AI coding assistant

The same flow runs through the proxymock MCP server. Its `generate` tool takes a schema file or endpoint URL, its `mocks` tool runs the analyze and accept loop from step 4, and `proxymock inventory` finds `.graphql` files and introspection results in a repository and suggests this path when a service has no recorded traffic.

## Next steps

*   [GraphQL semantic paths](https://docs.speedscale.com/guides/graphql/semantic-paths/) is the reference for addressing values inside a request.
*   [GraphQL vs gRPC](https://speedscale.com/blog/graphql-vs-grpc/) compares the two when you are still choosing, and the [gRPC version of this walkthrough](https://speedscale.com/blog/test-grpc-service-from-proto/) does the same from a `.proto`.
*   Point `proxymock coverage` at your own recording and schema, and see how many of your operations have ever been recorded failing.

## Table of Contents

## Stop writing API mocks by hand

proxymock records real traffic from your running app and replays it as mocks for HTTP, gRPC, Postgres, Kafka, and more. Install locally, activate once, and start recording.

[Install proxymock](https://docs.speedscale.com/proxymock/getting-started/installation/) [View the docs](https://docs.speedscale.com/proxymock/)
