Getting started
Quick start
New here? Watch the 3½-minute intro.
This page takes you from install to a first typed call. Start here when you want to try liaise in a project; The problem it solves says why you might.
Install
npm install liaise
liaise has no dependencies and uses the standard fetch. CI tests it on Node 20, 22 and 24, and Where it runs covers browsers, Bun, Deno, Cloudflare Workers and React Native.
- It is ESM only.
importit.require('liaise')fails withERR_PACKAGE_PATH_NOT_EXPORTED, so from CommonJS useawait import('liaise'). - Set TypeScript’s
moduleResolutiontobundler,node16ornodenext. liaise has three entry points, and the oldnodesetting finds only the first, soliaise/middlewareandliaise/testingdon’t resolve. - liaise is built and tested with TypeScript 5.8. CI also checks that its types compile on every TypeScript version from 4.7 to 6.0.
Coming from @iremlopsum/apify? That is liaise’s old name. Switching takes two steps, listed in MIGRATION.md.
Make your first call
Define two endpoints, create a client, and make a call. https://api.example.com stands for your API’s base URL.
import { createApi, defineRequest } from 'liaise'
type User = { id: string; name: string; email: string }
// 1. Describe your endpoints. The path decides which params are required.
const getUser = defineRequest<User>()({ method: 'GET', path: '/users/:id' })
const createUser = defineRequest<User, { name: string; email: string }>()({
method: 'POST',
path: '/users',
})
// 2. Create the client. There is no default timeout: without one, a server
// that never answers keeps the call waiting.
const api = createApi({
baseUrl: 'https://api.example.com',
requests: { getUser, createUser },
timeout: 10_000,
})
// 3. Call it. This never throws: you always get { data, error }.
const { data, error } = await api.getUser({ id: '42' })
if (error) {
// error.kind says what went wrong: 'http', 'network', 'timeout', ...
console.error(error.kind, error.status)
} else {
console.log(data.name) // data is a User here
}
// A POST sends its params as a JSON body.
const created = await api.createUser({ name: 'Grace', email: 'grace@example.com' })
if (!created.error) console.log(created.data.id)
defineRequest<User>()({ ... }) is two calls on purpose. The first takes the response type you write, and the second infers the params from the path. Defining endpoints explains why it can’t be one.
What you just got
-
The path decides the params.
/users/:idmakesidrequired, so a wrong name doesn’t compile:// @ts-expect-error: the path names the param `id`, not `userId` api.getUser({ userId: '42' })A call whose
idis stillundefinedat runtime returns an error before anything is sent. -
Every other param comes from the second type.
createUsertakes{ name: string; email: string }. It is aPOST, so it sends them as a JSON body. -
A hung server becomes an error only with a
timeout. There is no default, so the example sets 10 seconds on the client. When it passes, the call comes back withkind: 'timeout'(Set a deadline). -
datais typed fromdefineRequest<User>. You never annotate a call. -
Nothing throws, not even when you’re offline. Every failure comes back in
error, anderror.kindnames it:'http','network','timeout','abort','parse'or'middleware'. -
Checking
errorfirst narrowsdatatoUser, so you never writedata!.
Next: How it fits together names the pieces you just used and follows one call through them. If you have a task in mind, go straight to it: