ky HTTP client with hooks and retries, tested with MSW + Vitest: working versions (tested 2026-10-15)
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.
passed3 passed · 0 failed2026-10-15 11:54 UTC- Status
- passed
- Last passed
- 2026-10-15 11:54 UTC
- Tests
- 3 / 3
- 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 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("ky-msw-vitest") (install), or see them on GitHub.
{
"name": "ky-msw-vitest",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"ky": "2.2.0"
},
"devDependencies": {
"@types/node": "24.19.2",
"vitest": "5.0.4",
"msw": "3.0.3"
}
}
import ky from "ky";
export function createApi(prefixUrl: string, getToken: () => string) {
return ky.create({
prefix: prefixUrl,
timeout: 5000,
retry: { limit: 2, methods: ["get"], statusCodes: [500, 502, 503, 504], backoffLimit: 0 },
hooks: {
beforeRequest: [({ request }) => { request.headers.set("authorization", `Bearer ${getToken()}`); }],
},
});
}
export type Repo = { id: number; name: string; stars: number };
export const getRepo = (api: ReturnType<typeof createApi>, name: string) => api.get(`repos/${name}`).json<Repo>();
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { HTTPError } from "ky";
import { createApi, getRepo } from "../src/api";
const BASE = "https://api.example.com";
let calls = 0;
const server = setupServer();
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); calls = 0; });
afterAll(() => server.close());
const api = createApi(BASE, () => "tok");
describe("ky client", () => {
it("sends auth and parses json", async () => {
server.use(http.get(`${BASE}/repos/:name`, ({ params, request }) => {
expect(request.headers.get("authorization")).toBe("Bearer tok");
return HttpResponse.json({ id: 1, name: params.name, stars: 5 });
}));
expect(await getRepo(api, "pp")).toEqual({ id: 1, name: "pp", stars: 5 });
});
it("retries 503 then succeeds", async () => {
server.use(http.get(`${BASE}/repos/:name`, () => (++calls < 3 ? new HttpResponse(null, { status: 503 }) : HttpResponse.json({ id: 2, name: "x", stars: 0 }))));
expect((await getRepo(api, "x")).id).toBe(2);
expect(calls).toBe(3);
});
it("throws HTTPError on 404 without retrying", async () => {
server.use(http.get(`${BASE}/repos/:name`, () => { calls++; return new HttpResponse(null, { status: 404 }); }));
await expect(getRepo(api, "nope")).rejects.toBeInstanceOf(HTTPError);
expect(calls).toBe(1);
});
});
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
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
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.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
Hono + Valibot request validation (Vitest)
Validate JSON bodies and query strings in Hono with Valibot schemas via @hono/valibot-validator, with custom error responses, tested with Vitest.