Guide
Sending data
This page says where liaise puts each param you pass, and how it turns a body into what is sent. Read it when an API expects a particular query-string format, or when you send something other than JSON.
You pass one params object, and liaise puts each field where it belongs: in the path, the query string or the body. You never build a URL by hand.
import { createApi, defineRequest } from 'liaise'
type Item = { id: string; name: string }
const listItems = defineRequest<Item[], { page: number; tags: string[] }>()({
method: 'GET',
path: '/orgs/:org/items',
})
const createItem = defineRequest<Item, { name: string }>()({
method: 'POST',
path: '/orgs/:org/items',
})
const api = createApi({ baseUrl: 'https://api.example.com', requests: { listItems, createItem } })
await api.listItems({ org: 'acme', page: 2, tags: ['a', 'b'] })
// GET /orgs/acme/items?page=2&tags=a&tags=b
await api.createItem({ org: 'acme', name: 'Lamp' })
// POST /orgs/acme/items, with the JSON body {"name":"Lamp"}
org fills the path in both calls. For the GET, the other params go in the query string, and an array becomes a repeated key. For the POST, they go in a JSON body, and liaise sets Content-Type: application/json. An endpoint can swap the two with bodyAs, as Query string or body shows.
Query strings
| Params | Query string |
|---|---|
{ page: 1, limit: 20 } | ?page=1&limit=20 |
{ tags: ['a', 'b'] } | ?tags=a&tags=b |
{ filter: null } | (left out) |
{ filter: undefined } | (left out) |
{ meta: { nested: true } } | Refused |
{ since: new Date() } | Refused |
- Arrays become repeated keys (
tags=a&tags=b), the format most server frameworks read. nullandundefinedare left out.- A nested object is refused. There is no standard way to put one in a query string (brackets, dots and JSON are all in use), so liaise doesn’t guess. Flatten it first.
- A
Dateis refused too. APIs expect ISO 8601 or epoch milliseconds, so convert it yourself withdate.toISOString()ordate.getTime(). - A refused param sends nothing. You get an error
Resultwithkind: 'network'and aTypeErrorinerror.bodythat names the param.
A baseUrl can carry a query string of its own, such as a fixed api-version. Its params go in front of the call’s (baseUrl query merging).
Request bodies
liaise serializes the body from what you pass, and sets Content-Type for you unless you set one yourself.
import { createApi, defineRequest } from 'liaise'
type User = { id: string; name: string }
type Order = { id: string }
type Line = { sku: string; qty: number }
const updateUser = defineRequest<User, { name: string }>()({
method: 'PATCH',
path: '/users/:id',
})
const placeOrder = defineRequest<Order, { customer: { id: string }; lines: Line[] }>()({
method: 'POST',
path: '/orders',
})
const createItems = defineRequest<{ created: number }, { name: string }[]>()({
method: 'POST',
path: '/items/bulk?dryRun=false',
})
const api = createApi({
baseUrl: 'https://api.example.com',
requests: { updateUser, placeOrder, createItems },
})
await api.updateUser({ id: '7', name: 'Grace' })
// PATCH /users/7, with the JSON body {"name":"Grace"}
await api.placeOrder({ customer: { id: 'c1' }, lines: [{ sku: 'lamp', qty: 2 }] })
// POST /orders, with the JSON body {"customer":{"id":"c1"},"lines":[{"sku":"lamp","qty":2}]}
await api.createItems([{ name: 'Lamp' }, { name: 'Desk' }])
// POST /items/bulk?dryRun=false, with the JSON body [{"name":"Lamp"},{"name":"Desk"}]
- A path param fills the path and is left out of the body.
idgoes into/users/7, so the body is only{"name":"Grace"}. If your API also wantsidin the body, give the body field a different name from the path param. - Nested objects and arrays go in the body as JSON. Only the query string refuses them.
- An array as the params is the whole body. It is sent as a JSON array. An array has no named fields, so it can’t fill path params or a query string, and a call that needs either is refused before anything is sent.
- A fixed query string can go in the path, as
?dryRun=falsedoes here. Every param you pass goes either in the body or in the query string, never in both. For a query value that changes from call to call on a body request, see Send a query param with a JSON body.
| You pass | Body sent | Content-Type |
|---|---|---|
null/undefined | null | (none) |
string | as-is | text/plain |
FormData | as-is | (the browser sets the multipart boundary) |
URLSearchParams | as-is | application/x-www-form-urlencoded |
Blob | as-is | application/octet-stream |
ArrayBuffer | as-is | application/octet-stream |
Typed array, DataView, Buffer | as-is (sent as binary) | application/octet-stream |
ReadableStream | as-is (a streaming upload; duplex: 'half' is set for you) | application/octet-stream |
| Plain object or array | JSON.stringify() | application/json |
A ReadableStream body can be sent only once. A retry, from middleware or from result.retry(), returns an error telling you to read the stream into a Blob or ArrayBuffer first (details). Upload and download files shows a FormData upload.
What params can be
| You pass | What happens |
|---|---|
| Plain object | Split into path params, query string and body |
Map with string keys | Same as the object it spells |
| Class instance with fields | Same as a plain object (split by those fields even if the class also defines toJSON(); toJSON() is used only when there are no own fields) |
Class instance with only toJSON() | Sent as its JSON (body only; refused on a request whose params go in the query string) |
| Array | Sent as a JSON array body, such as [{"name":"a"}]. Refused on a request whose params go in the query string, or whose path has params, since an array has no named fields to fill them |
Typed array, DataView, Buffer, ReadableStream | Sent as the body, as in the table above (refused on a request whose params go in the query string) |
Set, a bare Date, a Map with non-string keys, a class with no fields | Refused: an error Result (kind: 'network') naming the type. Nothing is sent. |
A Map, Set or class with private state nested inside a JSON body is sent as {}, because that is what JSON.stringify does. Convert it first.
Headers, including your own Content-Type, follow the three levels of settings.
Next: Reading responses covers the other direction: how liaise reads what the server sends back.