How to Test Claude API Integration Locally
Testing a Claude API integration locally means verifying your request/response handling, error paths, streaming logic, and tool-use flows without burning real API credits or waiting on network latency for every iteration. The fastest way to do this is to separate your integration code from the live API using a thin abstraction layer, then validate against both a local mock server and a small set of real API calls before shipping.
This matters because Claude API bugs are rarely about the model's output — they're about how your code handles streaming chunks, retries, malformed JSON, rate limit headers, or multi-turn conversation state. You can catch almost all of that locally, cheaply, and fast.
Why Local Testing Matters for Claude Integrations
Every real API call costs money and takes a few hundred milliseconds to a few seconds. If your test suite hits the live endpoint on every run, you get:
- Slow CI pipelines
- Flaky tests due to model output variability
- Unnecessary API spend
- Difficulty testing edge cases (rate limits, timeouts, malformed responses)
A proper local testing setup fixes all four. The goal isn't to avoid the real API entirely — it's to test your integration logic locally and reserve real calls for a small smoke-test suite.
Set Up a Local Mock Server
The core technique is running a lightweight HTTP server that mimics the Claude API's response shape. You don't need anything fancy — a few routes in Express or a mock tool like msw (Mock Service Worker) or json-server will do.
// mock-server.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/v1/messages', (req, res) => {
const { stream } = req.body;
if (stream) {
res.setHeader('Content-Type', 'text/event-stream');
res.write('event: content_block_delta\n');
res.write('data: {"delta":{"text":"Hello"}}\n\n');
res.write('event: message_stop\n');
res.write('data: {}\n\n');
return res.end();
}
res.json({
id: 'msg_test_123',
content: [{ type: 'text', text: 'Mock response' }],
usage: { input_tokens: 10, output_tokens: 5 },
stop_reason: 'end_turn',
});
});
app.listen(4000, () => console.log('Mock Claude API on :4000'));
Point your app's base URL at http://localhost:4000 during tests instead of the real endpoint. Use an environment variable so this is a one-line swap:
const BASE_URL = process.env.CLAUDE_BASE_URL || 'https://api.anthropic.com';
This same pattern works whether you're calling the Anthropic API directly or through a gateway like SubToAPI — keep the base URL configurable and never hardcode it.
Test the Request Shapes You Actually Send
Don't just test "does it respond" — assert on the exact payload your code sends. This catches bugs like missing max_tokens, wrong role ordering in messages, or forgetting to pass system as a top-level field instead of inside the messages array.
test('sends correct message structure', async () => {
const requestBody = await captureRequestBody(() =>
sendMessage('What is the capital of France?')
);
expect(requestBody.model).toBe('claude-sonnet-4-5');
expect(requestBody.messages[0].role).toBe('user');
expect(requestBody.max_tokens).toBeGreaterThan(0);
});
Simulate Error Conditions
Real-world integrations fail on rate limits, timeouts, and malformed JSON far more often than they fail on bad prompts. Your mock server should let you force these conditions on demand:
app.post('/v1/messages', (req, res) => {
if (req.headers['x-test-scenario'] === 'rate-limit') {
return res.status(429).json({ error: { type: 'rate_limit_error' } });
}
if (req.headers['x-test-scenario'] === 'timeout') {
return; // never respond, test your timeout handling
}
if (req.headers['x-test-scenario'] === 'overloaded') {
return res.status(529).json({ error: { type: 'overloaded_error' } });
}
// normal response...
});
Trigger these scenarios from your test suite by setting a custom header, and assert your retry/backoff logic behaves correctly without ever touching the real API.
Test Streaming Logic Without a Live Connection
Streaming bugs are the most common issue teams run into locally, because it's tempting to test only the non-streaming path. Your mock server should emit real SSE-formatted chunks so you can verify your parser correctly accumulates text, handles content_block_start/content_block_delta/content_block_stop events, and detects message_stop.
const response = await fetch('http://localhost:4000/v1/messages', {
method: 'POST',
body: JSON.stringify({ stream: true, messages: [...] }),
});
const reader = response.body.getReader();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
fullText += parseSSEChunk(value);
}
expect(fullText).toContain('Hello');
If you're building against SubToAPI instead of calling Anthropic directly, the same mock-first approach applies — SubToAPI's streaming format mirrors the standard Claude message events, so your parsing logic transfers unchanged. See the streaming docs for the exact event shapes to mock.
Test Tool Use Locally
If your integration uses Claude's tool-calling features, mock the tool_use content block and verify your code correctly executes the tool and sends the tool_result back in the next turn:
app.post('/v1/messages', (req, res) => {
res.json({
content: [
{ type: 'tool_use', id: 'tool_1', name: 'get_weather', input: { city: 'Berlin' } },
],
stop_reason: 'tool_use',
});
});
Then assert your application calls the right local function and constructs a valid follow-up request with a tool_result block. Check the tools docs for the exact schema if you're wiring this through SubToAPI.
Run a Small Smoke Test Against the Real API
Once your mock-based tests pass, run a handful of real calls — maybe 5 to 10 — against the actual endpoint to confirm auth, model names, and response shapes still match reality. Keep this suite separate (e.g., npm run test:smoke) and don't run it on every commit.
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Say OK"}]
}'
If you're testing against SubToAPI specifically, a free trial key from signup is enough to run this smoke suite — check the quickstart for the full request/response reference and the messages docs for field-level details.
Keep Local and Production Configs in Sync
A common failure mode: your local mock diverges from the real API over time as Anthropic ships changes. Review your mock server's response shapes against the official docs every few months, or generate fixtures directly from real smoke-test responses so they stay accurate automatically.
// Capture a real response once, save as fixture
fs.writeFileSync('fixtures/messages-response.json', JSON.stringify(realResponse));
Then load that fixture into your mock server instead of hand-writing JSON.
Questions
Do I need a real API key to test my integration locally? No. Use a mock server for the bulk of your tests (request shape, error handling, streaming, retries), and reserve a small smoke-test suite with a real key to catch drift between your mocks and the live API.
How do I test streaming responses without hitting the real API? Run a local server that emits properly formatted SSE events (content_block_delta, message_stop, etc.) and point your client at it via an environment variable. This validates your parsing logic without network cost.
What's the biggest mistake teams make when testing Claude integrations? Only testing the happy path. Rate limits, timeouts, malformed JSON, and tool-use edge cases cause far more production incidents than bad model outputs — mock those scenarios explicitly in your test suite.