Networking#
TypeScript inherits the JavaScript networking story; the runtime is what matters (Node, Deno, Bun, browsers). See Networking for the runtime-level APIs.
This page focuses on TypeScript-specific patterns: typed clients, end-to-end type safety, and validated request and response schemas.
fetch with Types#
The simplest typed wrapper.
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as User;
}
The as User is a type assertion, not a check. The runtime will hand
you whatever the server actually sent, regardless of the type you wrote.
Validate at the boundary.
import { z } from "zod";
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof User>;
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return User.parse(await res.json());
}
End-to-End Type Safety#
TypeScript’s defining networking pattern: the client knows the API’s types without code generation.
tRPC, procedure types flow from server to client through TS imports.
Hono RPC, typed routes consumable by the client.
ts-rest, contract-first REST.
oRPC, OpenAPI-aware RPC with full TS types.
// server.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.number() }))
.query(({ input }) => db.user.findFirst({ where: { id: input.id } })),
});
export type AppRouter = typeof appRouter;
// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server";
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: "/api/trpc" })],
});
const user = await trpc.getUser.query({ id: 1 }); // fully typed
OpenAPI-Generated Clients#
When the API is REST and the contract lives in OpenAPI, generate a typed client.
openapi-typescript, generates types from OpenAPI.
openapi-fetch, typed
fetchagainst those types.orval, generates clients with TanStack Query / SWR / axios.
GraphQL#
urql, Apollo Client, graffle, clients.
GraphQL Code Generator, generates TS types from queries + schema.
The pattern that has stuck: write queries as .graphql files (or tagged
templates), run codegen, get fully-typed hooks / functions.
WebSockets with Types#
The browser WebSocket API is dynamically typed. To keep messages typed,
encode them as discriminated unions and validate on receipt.
import { z } from "zod";
const Message = z.discriminatedUnion("type", [
z.object({ type: z.literal("ping"), at: z.number() }),
z.object({ type: z.literal("chat"), from: z.string(), text: z.string() }),
]);
type Message = z.infer<typeof Message>;
ws.addEventListener("message", (e) => {
const parsed = Message.safeParse(JSON.parse(e.data));
if (!parsed.success) return;
handle(parsed.data);
});
gRPC#
connect-es, TS-first gRPC and Connect protocol.
@bufbuild/protobuf, modern Protobuf for TS.
@grpc/grpc-js, traditional gRPC for Node.
Server Frameworks#
The TS-first backend frameworks all expose their request and response types.
Hono, typed routes; runs anywhere.
Fastify, type providers (
@sinclair/typeboxor Zod) for full request/response typing.NestJS, decorator + DI; types via class metadata.
import { Hono } from "hono";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
const app = new Hono();
app.post(
"/users",
zValidator("json", z.object({ name: z.string() })),
(c) => {
const { name } = c.req.valid("json"); // typed
return c.json({ id: 1, name });
},
);
Pitfalls#
``as`` is not a check, assertions don’t validate. Parse at boundaries.
``any`` from JSON,
await res.json()isany; pin tounknownand validate.``fetch`` doesn’t throw on ``!res.ok``, check yourself.
Different runtimes, different APIs,
Node.fetchand browserfetchare similar but not identical (Node lacks credentials cookies, etc.). Test in the runtime you’ll deploy to.Type-only imports, when importing types across server/client, use
import type { ... }so bundlers can elide them.