Client
The Kaito client is a recent addition to the Kaito ecosystem. It’s still in the early stages of development. While we think it’s definitely stable, there may be missing features or unexpected behaviors.
Kaito provides a strongly-typed HTTP client that seamlessly integrates with your Kaito server. The client supports all HTTP methods, streaming responses, and Server-Sent Events (SSE) out of the box.
To ensure compatibility, always use matching versions of the client and server packages, as they are released together.
bun i @kaito-http/clientBasic Usage
Create a client instance by providing your API’s type and base URL:
const app = router.merge('/v1', v1);
const handle = app.serve();
export type App = typeof app;import {createKaitoHTTPClient} from '@kaito-http/client';
import type {App} from '../api/index.ts'; // Use `import type` to avoid runtime overhead
const api = createKaitoHTTPClient<App>({
base: 'http://localhost:3000',
});Making Requests
Normal Requests
The Kaito client ensures type safety across your entire API. It automatically:
- Validates input data (query parameters, path parameters, and request body)
- Constructs the correct URL
- Provides proper TypeScript types for the response
// `user` will be fully typed based on your route definition
const user = await api.get('/v1/users/:id', {
params: {
id: '123',
},
});
console.log(user);
await api.post('/v1/users/@me', {
body: {
name: 'John Doe', // Body schema is enforced by TypeScript
},
});Non-JSON Responses
For endpoints that return a Response instance, you must pass response: true to the request options. This is enforced at a compile time type level, so you
cannot accidentally forget to pass it. The option is needed so the runtime JavaScript does not assume the response is JSON.
const response = await api.get('/v1/response/', {
response: true,
});
const text = await response.text(); // or you could use .arrayBuffer() or .blob(), etcServer-Sent Events (SSE)
The client provides built-in support for SSE streams. You can iterate over the events using a for await...of loop:
// GET request with SSE
const stream = await api.get('/v1/sse_stream', {
sse: true, // sse: true is enforced at a compile time type level
query: {
content: 'Your streaming content',
},
});
for await (const event of stream) {
console.log('event', event.data);
}
// POST request with SSE
const postStream = await api.post('/v1/sse_stream', {
sse: true,
body: {
count: 20,
},
});
for await (const event of postStream) {
// Handle different event types
switch (event.event) {
case 'numbers':
console.log(event.data.digits); // TypeScript knows this is a number
break;
case 'data':
console.log(event.data.obj); // TypeScript knows this is an object
break;
case 'text':
console.log(event.data.text); // TypeScript knows this is a string
break;
}
}Cancelling Requests
You can use an AbortSignal to cancel a request
// Cancel requests using AbortSignal
const controller = new AbortController();
const user = await api.get('/v1/users/:id', {
params: {id: '123'},
signal: controller.signal,
});Error Handling
When a route throws an error, the client throws a KaitoClientHTTPError with detailed information about what went wrong:
.request: The originalRequestobject.response: TheResponseobject containing status code and headers.body: The error response with this structure:{ success: false, message: string, // Additional error details may be included }
Here’s how to handle errors effectively:
import {isKaitoClientHTTPError} from '@kaito-http/client';
try {
const response = await api.get('/v1/this-will-throw');
} catch (error: unknown) {
if (isKaitoClientHTTPError(error)) {
console.log('Error message:', error.message);
console.log('Status code:', error.response.status);
console.log('Error details:', error.body);
}
}Customizing Request Behavior
The client provides two powerful options for customizing how requests are made: fetch and before.
Request Preprocessing
The before option lets you modify requests before they are sent. This is perfect for:
- Adding authentication headers
- Setting up request tracking
- Modifying request parameters
const api = createKaitoHTTPClient<App>({
base: 'http://localhost:3000',
before: async (url, init) => {
// Set credentials
const request = new Request(url, {
...init,
credentials: 'include',
});
// Add authentication
request.headers.set('Authorization', `Bearer ${getToken()}`);
// Add tracking headers
request.headers.set('X-Request-ID', generateRequestId());
return request;
},
});You can combine both options for maximum flexibility:
const api = createKaitoHTTPClient<App>({
base: 'http://localhost:3000',
before: async (url, init) => {
const request = new Request(url, init);
request.headers.set('Authorization', `Bearer ${getToken()}`);
return request;
},
fetch: async request => {
const response = await fetch(request);
// Add response processing here
return response;
},
});Custom Fetch Implementation
You can provide a custom fetch implementation to override the default global fetch. This is useful when you need to:
- Use a different fetch implementation
- Add global request interceptors
- Modify how requests are made
const api = createKaitoHTTPClient<App>({
base: 'http://localhost:3000',
fetch: async request => {
// Use a custom fetch implementation
return await customFetch(request);
// Or modify the response if you really want to
const response = await fetch(request);
return new Response(response.body, {
status: response.status,
headers: {
...Object.fromEntries(response.headers),
'X-Custom-Header': 'value',
},
});
},
});