@honorest/client
v0.1.1
Published
Type-safe HTTP client for HonorestJS contracts - Automatically generated API client with full TypeScript support
Maintainers
Readme
@honorest/client
Type-safe HTTP client for HonorestJS contracts
@honorest/client provides an automatically generated, type-safe HTTP client for your API contracts. Built on top of @honorest/contract, it ensures complete type safety from server to client with zero code generation.
Features
🎯 Fully Type-Safe - Complete TypeScript inference for inputs and outputs
🚀 Auto-Generated API - Client methods generated from contracts
🔒 Type-Safe Errors - Typed error handling based on contract error schemas
⚡ Fetch-Based - Uses native fetch API (works in browsers and Node.js 18+)
🎣 Interceptors - Request/response/error interceptors
📦 Lightweight - Minimal dependencies, tree-shakeable
Installation
npm install @honorest/client @honorest/contract
# or
yarn add @honorest/client @honorest/contract
# or
pnpm add @honorest/client @honorest/contract
# or
bun add @honorest/client @honorest/contractQuick Start
1. Define Your Contract
First, create your API contract using @honorest/contract:
// contracts/users.contract.ts
import { defineContract, endpoint, defineApp } from '@honorest/contract'
import { z } from 'zod'
const UsersContract = defineContract({
name: 'users',
path: '/users',
endpoints: {
getUser: endpoint({
method: 'GET',
path: '/:id',
params: z.object({ id: z.string().uuid() }),
output: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}),
errors: {
404: z.object({ message: z.string() })
}
}),
createUser: endpoint({
method: 'POST',
path: '/',
body: z.object({
name: z.string().min(1),
email: z.string().email()
}),
output: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
})
})
}
})
export const AppContract = defineApp({
prefix: '/api',
contracts: {
users: UsersContract
}
})2. Create the Client
import { createClient } from '@honorest/client'
import { AppContract } from './contracts'
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000'
})3. Make Type-Safe API Calls
// GET request with path parameters
const user = await api.users.getUser({ params: { id: '123' }})
// ^? { id: string, name: string, email: string }
// POST request with body
const newUser = await api.users.createUser({
body: { name: 'John Doe', email: '[email protected]' }
})
// ^? { id: string, name: string, email: string }API Reference
createClient(app, config)
Creates a type-safe HTTP client from an app contract.
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000',
headers: {
'Authorization': 'Bearer token'
},
fetch: customFetch, // Optional custom fetch implementation
onRequest: async (url, init) => {
console.log('Request:', url)
},
onResponse: async (response) => {
console.log('Response:', response.status)
},
onError: async (error) => {
console.error('Error:', error)
}
})Config Options
| Option | Type | Description |
|--------|------|-------------|
| baseUrl | string | Required. Base URL for the API |
| headers | Record<string, string> | Default headers for all requests |
| fetch | typeof fetch | Custom fetch implementation (useful for testing) |
| onRequest | (url, init) => void \| Promise<void> | Request interceptor |
| onResponse | (response) => void \| Promise<void> | Response interceptor |
| onError | (error) => void \| Promise<void> | Error interceptor |
Error Handling
ClientError
Thrown when the API returns an error response (4xx or 5xx).
import { ClientError } from '@honorest/client'
try {
await api.users.getUser({ params: { id: 'invalid' }})
} catch (error) {
if (error instanceof ClientError) {
console.log(error.status) // 404
console.log(error.data) // { message: "User not found" }
console.log(error.message) // "HTTP 404: Not Found"
console.log(error.request) // { method: 'GET', url: '...', headers: {} }
// Helper methods
if (error.is(404)) {
console.log('Not found!')
}
if (error.isClientError()) {
console.log('4xx error')
}
if (error.isServerError()) {
console.log('5xx error')
}
}
}NetworkError
Thrown when the request fails before reaching the server (network issues, CORS, etc.).
import { NetworkError } from '@honorest/client'
try {
await api.users.getUser({ params: { id: '123' }})
} catch (error) {
if (error instanceof NetworkError) {
console.log('Network error:', error.message)
console.log('Cause:', error.cause)
}
}ValidationError
Thrown when response doesn't match the contract (if validation is enabled).
import { ValidationError } from '@honorest/client'
try {
await api.users.getUser({ params: { id: '123' }})
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation error:', error.message)
console.log('Errors:', error.errors)
}
}Advanced Usage
Custom Headers Per Request
const user = await api.users.getUser({
params: { id: '123' },
headers: {
'X-Custom-Header': 'value'
}
})Query Parameters
// Endpoint definition
listUsers: endpoint({
method: 'GET',
path: '/',
query: z.object({
page: z.number().default(1),
limit: z.number().default(20),
search: z.string().optional()
}),
output: z.object({
users: z.array(UserSchema),
total: z.number()
})
})
// Client usage
const result = await api.users.listUsers({
query: {
page: 2,
limit: 50,
search: 'john'
}
})Authentication
// Global auth token
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000',
headers: {
'Authorization': `Bearer ${token}`
}
})
// Or use request interceptor for dynamic tokens
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000',
onRequest: async (url, init) => {
const token = await getAuthToken()
init.headers = {
...init.headers,
'Authorization': `Bearer ${token}`
}
}
})Request/Response Logging
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000',
onRequest: async (url, init) => {
console.log(`→ ${init.method} ${url}`)
},
onResponse: async (response) => {
console.log(`← ${response.status}`)
},
onError: async (error) => {
console.error('✗ Request failed:', error.message)
}
})Custom Fetch Implementation
Useful for testing or adding custom behavior:
const api = createClient(AppContract, {
baseUrl: 'http://localhost:3000',
fetch: async (url, init) => {
// Custom fetch logic
console.log('Custom fetch:', url)
return fetch(url, init)
}
})Type Inference
The client provides complete type inference:
import type { ClientInput, ClientOutput } from '@honorest/client'
// Get input type for an endpoint
type GetUserInput = ClientInput<typeof UsersContract.endpoints.getUser>
// { params: { id: string } }
// Get output type for an endpoint
type GetUserOutput = ClientOutput<typeof UsersContract.endpoints.getUser>
// { id: string, name: string, email: string }Best Practices
1. Create a Shared API Instance
// api.ts
import { createClient } from '@honorest/client'
import { AppContract } from '@myapp/api-contract'
export const api = createClient(AppContract, {
baseUrl: process.env.API_URL || 'http://localhost:3000',
headers: {
'Content-Type': 'application/json'
}
})2. Handle Errors Consistently
async function fetchUser(id: string) {
try {
return await api.users.getUser({ params: { id }})
} catch (error) {
if (error instanceof ClientError) {
if (error.is(404)) {
return null // User not found
}
throw new Error(`Failed to fetch user: ${error.message}`)
}
throw error
}
}3. Use TypeScript Strict Mode
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}Troubleshooting
CORS Errors
If you're getting CORS errors, make sure your server allows requests from your client origin:
// Server-side (HonorestJS)
app.use('*', cors({
origin: 'http://localhost:5173', // Your client URL
credentials: true
}))TypeScript Errors
Make sure you have the correct versions:
@honorest/contract:^0.1.1typescript:^5.8.3
Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © HonorestJS
