Basic Usage
Every request returns a Go-style { data, error }: don't use try/catch to detect business failures (only network-level exceptions are thrown).
Creating a Client
import { createClient, eden } from '@vafast/api-client'
import { createApiClient } from './api.generated' // generated by the CLI, optional
const client = createClient({
baseURL: 'https://api.example.com',
timeout: 30_000,
headers: { 'X-App-Id': 'my-app' },
})
// with generated types
const api = createApiClient(client)
// or a manual contract
// const api = eden<Api>(client)Chained Calls
Only the last segment of the chain, the one that is invoked, is the HTTP verb; every property before it is treated as a path segment.
// GET /users?page=1
const { data, error } = await api.users.get({ page: 1 })
// POST /users
const created = await api.users.post({ name: 'Ada' })
// GET /users/123
const one = await api.users({ id: '123' }).get()
// PUT /users/123
const updated = await api.users({ id: '123' }).put({ name: 'Ada Lovelace' })
// DELETE /users/123
const removed = await api.users({ id: '123' }).delete()| Code | Request |
|---|---|
api.users.get(query?) | GET /users |
api.users.post(body) | POST /users |
api.users({ id }).get() | GET /users/:id |
api.users.find.post(body) | POST /users/find |
Caveat: Path Segments Named Like Verbs
A path may contain segments named get / post / put / patch / delete / head / options (e.g. POST /prices/delete). This library does not use prefixes like $post to avoid the clash. The convention is:
- A middle segment named
deleteis still just part of the path - You must add the real HTTP verb as the final call in the chain
// POST /prices/delete
await api.prices.delete.post({ ids: ['1', '2'] })
// GET /reports/export
await api.reports.export.get({ format: 'csv' })
// DELETE /prices/delete (both the last path segment and the verb are delete)
await api.prices.delete.delete({ ids: ['1'] })| Easily confused | Actual meaning |
|---|---|
api.prices.delete() | DELETE /prices (delete is treated as the verb) |
api.prices.delete.post(...) | POST /prices/delete (delete is the path, post is the verb) |
Autocompletion and types already distinguish "path nodes" from "method definitions", so the second form in the table is suggested correctly. If a path segment really must be called delete or similar, remember: always follow the path segment with .get / .post / ….
Go-Style Error Handling
const { data, error } = await api.users.get({ page: 1 })
if (error) {
// error: { code: number; message: string; type?: ErrorType; details?: ErrorDetail[] }
console.error(`${error.code}: ${error.message}`)
return
}
// data is non-null here
console.log(data.users)Branch on the status code:
const { data, error } = await api.users.post(form)
if (error) {
switch (error.code) {
case 401:
redirectToLogin()
break
case 403:
showPermissionDenied()
break
case 422:
// see validation errors below
break
default:
showError(error.message)
}
return
}
console.log(data)422 Schema Validation Errors
Consistent with the server: HTTP 422 + details. Use the helpers to bind them to a form:
import { isValidationError, mapDetailsToFormFields } from '@vafast/api-client'
const { error } = await api.users.post(formData)
if (error && isValidationError(error)) {
formRef.setFields(mapDetailsToFormFields(error.details))
return
}error.type | Meaning | Typical code |
|---|---|---|
network | Cannot connect | 0 |
timeout | Timed out | 408 |
abort | Cancelled | 0 |
server | 4xx / 5xx | HTTP status code |
parse | Failed to parse the response | 0 |
Per-Request Config
Pass a RequestConfig as the second argument (e.g. cancellation, timeout, extra headers):
const controller = new AbortController()
const { data, error } = await api.users.get(
{ page: 1 },
{
signal: controller.signal,
timeout: 5_000,
headers: { Authorization: `Bearer ${token}` },
},
)
// cancel
controller.abort()Middleware
import { createClient, defineMiddleware, retryMiddleware, loggerMiddleware } from '@vafast/api-client'
const auth = defineMiddleware(async (ctx, next) => {
const token = localStorage.getItem('token')
if (token) ctx.headers.set('Authorization', `Bearer ${token}`)
const res = await next()
if (res.status === 401) {
// token expired: refresh or redirect to login
}
return res
})
const client = createClient({ baseURL: '/api' })
.use(auth)
.use(retryMiddleware({ count: 3, delay: 1000 }))
.use(loggerMiddleware({ prefix: '[API]' }))SSE
Chain .sse() after a regular method (it goes through the same middleware):
const sub = api.chat.stream.post({ prompt: 'Hello' }).sse({
onMessage: (chunk) => console.log(chunk),
onError: (error) => console.error(error),
onClose: () => console.log('done'),
})
sub.unsubscribe()Full Example
import { createClient, isValidationError, mapDetailsToFormFields } from '@vafast/api-client'
import { createApiClient } from './api.generated'
const api = createApiClient(
createClient({ baseURL: '/api', timeout: 30_000 }),
)
async function loadUsers(page: number) {
const { data, error } = await api.users.get({ page })
if (error) {
showError(error.message)
return null
}
return data
}
async function createUser(form: { name: string; email: string }) {
const { data, error } = await api.users.post(form)
if (error) {
if (isValidationError(error)) {
formRef.setFields(mapDetailsToFormFields(error.details))
return null
}
showError(error.message)
return null
}
return data
}Next Steps
- Advanced Usage — multi-tenancy, token refresh, multiple services,
client.request, uploads - Overview
- Testing
- CLI