openapi-fetch typed client with MSW: working versions (tested 2026-10-15)
Call a typed OpenAPI paths client with openapi-fetch against MSW handlers, covering GET/POST and error status offline.
passed2 passed · 0 failed2026-10-15 16:03 UTC- Status
- passed
- Last passed
- 2026-10-15 16:03 UTC
- Tests
- 2 / 2
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("openapi-fetch-msw") (install), or see them on GitHub.
{
"name": "openapi-fetch-msw",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {"openapi-fetch":"0.17.0"},
"devDependencies": {"@types/node":"26.6.5","tsx":"4.23.15","msw":"3.0.3"}
}
import createClient from "openapi-fetch";
import type { paths } from "./schema.ts";
export function createApi(baseUrl: string) {
const client = createClient<paths>({ baseUrl });
return {
async list(q?: string) {
const { data, error } = await client.GET("/items", { params: { query: { q } } });
if (error) throw error;
return data ?? [];
},
async create(name: string) {
const { data, error, response } = await client.POST("/items", {
body: { name },
});
if (error) throw error;
if (!data) throw new Error(`create failed: ${response.status}`);
return data;
},
async get(id: number) {
const { data, error, response } = await client.GET("/items/{id}", {
params: { path: { id } },
});
if (response.status === 404) throw Object.assign(new Error("not found"), { status: 404 });
if (error) throw error;
return data!;
},
};
}
/** Minimal OpenAPI paths typing for openapi-fetch (hand-written stand-in for openapi-typescript output). */
export type paths = {
"/items": {
get: {
parameters: { query?: { q?: string } };
responses: {
200: { content: { "application/json": { id: number; name: string }[] } };
};
};
post: {
requestBody: { content: { "application/json": { name: string } } };
responses: {
201: { content: { "application/json": { id: number; name: string } } };
};
};
};
"/items/{id}": {
get: {
parameters: { path: { id: number } };
responses: {
200: { content: { "application/json": { id: number; name: string } } };
404: { content: { "application/json": { error: string } } };
};
};
};
};
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { createApi } from "../src/client.ts";
const items = [{ id: 1, name: "bolt" }];
const server = setupServer(
http.get("http://local/items", ({ request }) => {
const q = new URL(request.url).searchParams.get("q");
return HttpResponse.json(q ? items.filter((i) => i.name.includes(q)) : items);
}),
http.post("http://local/items", async ({ request }) => {
const body = (await request.json()) as { name: string };
const item = { id: items.length + 1, name: body.name };
items.push(item);
return HttpResponse.json(item, { status: 201 });
}),
http.get("http://local/items/:id", ({ params }) => {
const item = items.find((i) => i.id === Number(params.id));
if (!item) return HttpResponse.json({ error: "missing" }, { status: 404 });
return HttpResponse.json(item);
}),
);
before(() => server.listen({ onUnhandledRequest: "error" }));
after(() => server.close());
test("lists filters and creates", async () => {
const api = createApi("http://local");
assert.equal((await api.list()).length, 1);
assert.equal((await api.list("bo"))[0].name, "bolt");
const created = await api.create("nut");
assert.equal(created.name, "nut");
assert.equal((await api.get(created.id)).name, "nut");
});
test("404 becomes error", async () => {
const api = createApi("http://local");
await assert.rejects(() => api.get(999), (e: any) => e.status === 404);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
graphql-request client with typed operations, mocked by MSW graphql handlers
Call a GraphQL API with graphql-request (variables, headers, error handling) and test offline with MSW's graphql.link(url).query/mutation handlers (msw 3 API). graphql stays on 16.x: graphql-request 7 peers on graphql 14 - 16.
ky HTTP client with hooks and retries, tested with MSW + Vitest
A ky 2 API client (prefix option, state-object hooks) with auth hooks, retry on 5xx and typed JSON, tested offline with MSW's Node server in Vitest.
ofetch HTTP client mocked with MSW
Call APIs with ofetch against an MSW 2 HTTP worker in Node, covering JSON responses, query params and error status mapping offline.
Redux Toolkit + RTK Query with tag invalidation and optimistic updates, tested with MSW
An RTK Query API slice with auth headers from Redux state, tag-based cache invalidation and an optimistic update with rollback, tested offline in Vitest against MSW 3 request handlers without rendering React.