Skip to main content

HTTP client quickstart

Altstack's TypeScript HTTP clients take two Zod-backed maps:

  • Request declares the allowed endpoints, methods, path parameters, query parameters, and JSON bodies.
  • Response declares the JSON schema for each documented status code.

The Fetch and Ky adapters share the same ApiClient behavior. This quickstart uses Fetch because Node.js 18 and modern browsers already provide it.

1. Install​

pnpm add @alt-stack/http-client-fetch zod
pnpm add -D tsx typescript @types/node

The client packages require Zod 4. Run this example on Node.js 18 or newer so the standard Fetch API is available.

2. Create a complete local example​

Save this as quickstart.ts. It starts a temporary HTTP server, calls it through the typed client, and shuts the server down.

import { createServer } from "node:http";
import { createApiClient } from "@alt-stack/http-client-fetch";
import { z } from "zod";

const Request = {
"/users/{id}": {
GET: {
params: z.object({ id: z.string() }),
query: z.object({ includeProfile: z.boolean().optional() }),
},
},
} as const;

const Response = {
"/users/{id}": {
GET: {
"200": z.object({ id: z.string(), name: z.string() }),
"404": z.object({
error: z.object({ code: z.literal("NOT_FOUND"), message: z.string() }),
}),
},
},
} as const;

const server = createServer((request, response) => {
response.setHeader("content-type", "application/json");
if (request.url === "/users/u_1?includeProfile=true") {
response.end(JSON.stringify({ id: "u_1", name: "Ada" }));
return;
}

response.statusCode = 404;
response.end(JSON.stringify({
error: { code: "NOT_FOUND", message: "User not found" },
}));
});

await new Promise<void>((resolve) => server.listen(3100, resolve));

try {
const client = createApiClient({
baseUrl: "http://127.0.0.1:3100",
Request,
Response,
});

const result = await client.get("/users/{id}", {
params: { id: "u_1" },
query: { includeProfile: true },
});

if (result.success) {
console.log(result.code, result.body.name); // 200 Ada
} else if (result.code === "404") {
console.error(result.error.error.message);
} else {
console.error("Unexpected response", result.code, result.error);
}
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve());
});
}

Run it:

pnpm tsx quickstart.ts

Expected output:

200 Ada

3. Understand the contract​

client.get() accepts only endpoints that declare GET. The endpoint literal makes params, query, and the response union specific to that route. The client:

  1. validates path parameters and query data with the request schemas;
  2. interpolates {id} and builds the query string;
  3. executes the request;
  4. validates a documented response body;
  5. returns a union discriminated by success and then by code.

For POST, PUT, and PATCH, a supplied body is validated and sent as JSON. All methods derive body requirements from the request schema: required bodies must be supplied, schemas accepting undefined allow omission, and routes without a body schema reject payloads. Requests without a body send no default Content-Type header.

For example, when POST /v1/jobs/{id}/cancel has no OpenAPI requestBody, both Fetch and Ky clients accept:

await client.post("/v1/jobs/{id}/cancel", { params: { id: "job-1" } });

4. Use generated maps​

In an application, generate Request and Response from OpenAPI rather than maintaining them twice:

pnpm add -D @alt-stack/zod-openapi
pnpm zod-openapi ./openapi.json --output ./src/generated-api.ts

Then import both values from the generated module. Treat that module as generated output and regenerate it when the OpenAPI document changes.

Choose another runtime​

Continue with HTTP client common patterns or the core API Documentation.