Guide
Reading responses
This page says how liaise reads a response body and what you get in data. Read it when an endpoint returns something other than JSON, such as a file or a CSV, or nothing at all.
A response can be JSON, text or a file. You say which with responseType on the endpoint, and liaise reads the body for you:
import { createApi, defineRequest } from 'liaise'
type User = { id: string; name: string }
// JSON is the default.
const getUser = defineRequest<User>()({ method: 'GET', path: '/users/:id' })
// A CSV export comes back as a string.
const exportCsv = defineRequest<string>()({ method: 'GET', path: '/reports/:id/csv', responseType: 'text' })
// A file comes back as a Blob.
const downloadFile = defineRequest<Blob>()({ method: 'GET', path: '/files/:id', responseType: 'blob' })
// A 204 No Content has no body, so data is undefined.
const deleteUser = defineRequest<undefined>()({
method: 'DELETE',
path: '/users/:id',
responseType: 'none',
})
const api = createApi({
baseUrl: 'https://api.example.com',
requests: { getUser, exportCsv, downloadFile, deleteUser },
})
api.getUser gives you a parsed User, api.exportCsv a string, api.downloadFile a Blob, and api.deleteUser gives data: undefined when the server answers 204.
responseType | How the body is read | data |
|---|---|---|
'json' (default) | response.text(), then JSON.parse() | the parsed value |
'text' | response.text() | string |
'blob' | response.blob() | Blob |
'arrayBuffer' | response.arrayBuffer() | ArrayBuffer |
'formData' | response.formData() | FormData |
'none' | not read (the stream is cancelled) | undefined |
Empty bodies and null
- An empty body under
'json'is a'parse'error. You declared JSON and the server sent none, so no value could honestly match your type. The error has the response’s own status (a 204 reports 204), theresponse, and''inerror.body. - A literal
nullbody is not empty. It is valid JSON, so the call succeeds withdata: null.
Endpoints that send no body
'none'is for an endpoint that sends no body on success, such as aDELETEthat answers 204, or 200 with an empty body. liaise reads nothing,dataisundefined, and a body the server sends anyway is discarded. Its stream is cancelled, which frees the connection.- Declare
'none'with the response typeundefined.defineRequestenforces this (Defining endpoints). Without defineRequest it is only a convention, anddataisundefinedat runtime whatever type you wrote.
Error bodies
A non-2xx body is still read into error.body, because an error body usually explains what went wrong. It is read the same way as a success body, except under 'none': there it is read as JSON, and error.body is null when it isn’t JSON.
const { error } = await api.deleteUser({ id: '42' })
if (error) {
// A 409 { "error": "already deleted" } lands in error.body,
// even though deleteUser declares responseType: 'none'.
console.error(error.status, error.body)
}
error.body is typed unknown, because nothing promises what shape a server’s errors take. To use one, check it first. A type guard narrows it:
type Problem = { message: string; code?: string }
function isProblem(body: unknown): body is Problem {
return typeof body === 'object' && body !== null && typeof (body as Problem).message === 'string'
}
const { error } = await api.getUser({ id: '42' })
if (error?.kind === 'http' && isProblem(error.body)) {
show(error.body.message) // error.body is a Problem here
}
The validator you use for responses can do the same check. What error.body holds for every kind of failure is listed under ApiError.
Next: Validating responses checks at runtime that the body has the shape your type promises.