The Other Side of the API
TypeScript programs are not only servers — they call APIs too: fetching data, talking to services, integrating third parties. The client is fetch, built into Node.js, and three habits:
1. make the request (await fetch(url) for a GET, or fetch(url, { method, headers, body }) for full control),
2. check res.status before trusting the body — fetch does not throw on 404 or 500,
3. read the body once, with await res.json() or await res.text() — never both.
The demos below call a small local server started inside the same program, so the outputs are stable — the client code is exactly what you would point at a real URL.
GET and Decode
The everyday task: GET a URL, check the status, and turn the JSON body into data with await res.json(). That call returns any, so an as UserRecord[] tells the type checker what you expect to have.
Fetch a list into objects
The response body becomes a UserRecord[], and from there it is ordinary TypeScript data — looped and printed field by field.
Output: TypeScript file and captured output
import { createServer } from "node:http"
interface UserRecord {
id: number
name: string
}
async function main(): Promise<void> {
// A small local server stands in for the API this client calls.
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" })
res.end('[{"id": 1, "name": "Ana"}, {"id": 2, "name": "Ben"}]')
})
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
const res = await fetch(`${base}/users`)
// Check the status before trusting the body.
if (res.status !== 200) {
console.log("unexpected status:", res.status, res.statusText)
server.close()
return
}
const users = (await res.json()) as UserRecord[]
for (const user of users) {
console.log(`user ${user.id}: ${user.name}`)
}
server.close()
}
main()
POST with a JSON Body
Sending data is the mirror image: stringify the object, pass it as body with the right content type — then read the server's answer, which here carries the id the server assigned.
Send an object, read the answer
The object goes out as JSON and the created record comes back with its new id 7 — the whole write path of an API client in one small program. Node.js prints the parsed object as { id: 7, name: 'Cleo' }.
Output: TypeScript file and captured output
import { createServer, type IncomingMessage } from "node:http"
interface UserRecord {
id: number
name: string
}
async function readBody(req: IncomingMessage): Promise<string> {
const chunks: Buffer[] = []
for await (const chunk of req) chunks.push(chunk as Buffer)
return Buffer.concat(chunks).toString()
}
async function main(): Promise<void> {
// A small local server stands in for the API: it assigns the id.
const server = createServer(async (req, res) => {
const incoming = JSON.parse(await readBody(req)) as { name: string }
const created: UserRecord = { id: 7, name: incoming.name }
res.writeHead(201, { "Content-Type": "application/json" })
res.end(JSON.stringify(created))
})
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
const res = await fetch(`${base}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Cleo" }),
})
const created = (await res.json()) as UserRecord
console.log("status: ", res.status, res.statusText)
console.log("created:", created)
server.close()
}
main()