Installing the middleware takes about five minutes. The step that requires deliberate thought is configuring your masking rules first, before you capture anything. Once traffic starts flowing, any field not covered by a rule is retained in its original form, so the sequence matters.
Prerequisites
You need a Stubsmith project and an API key. Create a project from the dashboard at app.stubsmith.dev, then mint a key from the Projects page. Keep the key in an environment variable; it never belongs in source code. Full setup steps are at docs.stubsmith.dev.
You also need the ingest endpoint URL. The middleware reads this from STUBSMITH_URL; the correct value for your project is available in the dashboard.
Step 1: Add the capture middleware
Copy examples/express-middleware.js from the Stubsmith SDK repository into your project, for example as stubsmith-middleware.js. It is a small, self-contained file you can read and audit before trusting it. No package installation is required.
The snippet below reflects what that file does:
const express = require('express');
const captureMiddleware = require('./stubsmith-middleware');
const app = express();
// Mount before your routes so every response is captured
app.use(captureMiddleware({
url: process.env.STUBSMITH_URL,
key: process.env.STUBSMITH_API_KEY,
}));
// ... your routes
The middleware attaches a finish listener to each response. When the response completes, it assembles a payload containing the HTTP method, URL, request headers and body, response status code, and elapsed duration, then POSTs that payload to the ingest service with a Bearer token in the Authorization header. Errors from the ingest POST are swallowed, so a capture failure never affects the request being served.
The ingest endpoint is plain HTTP POST /v1/captures. The Express middleware is one convenience wrapper around that endpoint; the protocol is the same regardless of client.
Step 2: Define masking rules before capturing anything
This is the step most teams skip, and it is the one that matters most.
The capture middleware applies your masking rules in-process, before the payload is assembled for transmission. Rules use RE2 syntax and run against both field names and field values. But rules only cover what you tell them to cover. If you start capturing before your rules are correct, fields you haven’t listed pass through in their original form.
The rules configuration has two sections. field_masks lists field key names to blank regardless of their value. regex_masks lists patterns that match against field values, replacing anything that matches with a safe placeholder. Here is a starting configuration that covers the most common cases:
{
"field_masks": [
"password", "passwd", "token", "access_token",
"authorization", "auth", "ssn", "credit_card", "cvv",
"email", "phone", "address", "name"
],
"regex_masks": [
{
"pattern": "\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b",
"replace": "user@example.com"
},
{
"pattern": "\\b\\d{16}\\b",
"replace": "0000000000000000"
},
{
"pattern": "Bearer\\s+[A-Za-z0-9\\-\\._~\\+\\/=]{8,}",
"replace": "Bearer <masked>"
}
]
}
Upload this configuration through the Stubsmith dashboard or API before enabling the middleware in any environment that processes real user data. See docs.stubsmith.dev for the exact upload steps.
Masking is shape-preserving by design: strings become placeholder strings, numbers become 0, booleans become false. Your fixtures remain structurally valid after masking: validators and parsers in your codebase continue to work because the field is present and correctly typed.
If you set the STUBSMITH_MASK_SALT environment variable, masked values instead use format-preserving placeholders derived from a keyed hash of the original value: a masked email becomes a syntactically valid email at your project’s placeholder domain, a masked UUID becomes a different valid UUID, a masked IBAN carries a correct mod-97 checksum. The placeholder is deterministic (the same input value always produces the same placeholder), so two fields that held the same production value still match after masking. This is opt-in; omitting the variable keeps the default constant placeholders ("<masked>", 0, false). Note that low-cardinality types such as currency_code, country_code, and booleans always use constant placeholders regardless of the salt setting, because a keyed hash over a tiny value space is recoverable.
The regex_masks patterns catch sensitive content wherever it appears in the payload, regardless of field name. An email address that lands in a notes field won’t be caught by a field_masks rule, but the email regex pattern covers it. This is the second layer of the masking model: match by what the value looks like, not just where it lives.
Step 3: Set environment variables and deploy
export STUBSMITH_URL=https://ingest.stubsmith.dev/v1/captures
export STUBSMITH_API_KEY=sk-your-project-key
Both variables are read at middleware instantiation. Use whatever secret-management mechanism your deployment stack provides: Kubernetes Secrets, a secrets manager, Heroku config vars. The API key is a bearer credential; treat it like a password.
Deploy to a staging environment first. Avoid pointing live production traffic at the ingest service until you have verified your masking rules are complete.
Step 4: Verify captures are arriving
After deploying the middleware and sending a few requests through your application, open the Captures section of your project in the Stubsmith dashboard. You should see entries with the correct method, path, and status code.
Click into a capture and inspect the recorded body. Any field that should be masked should show its placeholder value: "<masked>" for strings (or a format-preserving placeholder if STUBSMITH_MASK_SALT is set), 0 for numbers, false for booleans, the replacement string for regex matches. If you see an unmasked value that should be protected, update your rules configuration and re-test before continuing.
The ingest service deduplicates traffic by structural fingerprint. Two requests with the same method, path, and response shape but different values are grouped under a single fingerprint. You accumulate one canonical example of each unique traffic shape rather than thousands of near-identical copies. Fingerprints are computed from the masked payload, so cleartext values never participate in fingerprint computation.
What comes next
Once captures are accumulating and your masking rules are verified, run stubsmith pull to write .stubsmith/bundle.json to your repository. That file is all the Python SDK needs to replay recorded responses inside your test process : no stub server, no port to allocate. Tests call your application code normally; the SDK intercepts the outbound requests calls and returns the recorded responses.
See Replaying API stubs in GitHub Actions for the next step, and Writing masking rules that fail closed for a deeper look at rule coverage and the fail-closed posture that prevents sensitive fields from slipping through.