Overview
@contract-kit/client provides type-safe client adapters for making contract-based HTTP requests. It generates fully typed API clients from your contracts with automatic validation and error handling.
Prefer the contract-kit meta package for new projects. It re-exports
@contract-kit/client along with core, application, ports, and more.
Installation
npm install @contract-kit/client @contract-kit/core zod
Quick Start
Creating a Client
import { createClient } from " @contract-kit/client " ;
export const apiClient = createClient ({
baseUrl : " https://api.example.com " ,
headers : async () => ({
" x-app-version " : " 1.0 " ,
Authorization : `Bearer ${ getToken () } ` ,
}),
});
Making Requests
import { apiClient } from " @/lib/api-client " ;
import { getTodo, createTodo, listTodos } from " @/contracts/todos " ;
// GET request with path params
const todo = await apiClient
. endpoint (getTodo)
. call ({ path : { id : " 123 " } });
// POST request with body
const newTodo = await apiClient
. endpoint (createTodo)
. call ({ body : { title : " Learn Contract Kit " } });
// GET request with query params
const todos = await apiClient
. endpoint (listTodos)
. call ({ query : { completed : false , limit : 10 } });
Configuration
Client Options
const client = createClient ({
// Required: Base URL for all requests
baseUrl : string;
// Optional: Headers function (can be async)
headers ?: () => Promise < Record < string, string>> | Record<string, string>;
// Optional: Custom fetch implementation
fetch? : typeof fetch;
});
Headers can be static or dynamically computed:
// Static headers
const client = createClient ({
baseUrl : " https://api.example.com " ,
headers : () => ({
" Content-Type " : " application/json " ,
}),
});
// Dynamic headers with authentication
const client = createClient ({
baseUrl : " https://api.example.com " ,
headers : async () => {
const token = await getAuthToken ();
return {
Authorization : `Bearer ${ token } ` ,
" x-request-id " : crypto. randomUUID (),
};
},
});
Custom Fetch
Use a custom fetch implementation for advanced scenarios:
import { createClient } from " @contract-kit/client " ;
const client = createClient ({
baseUrl : " https://api.example.com " ,
fetch : async ( url , init ) => {
console. log ( " Fetching: " , url);
const response = await fetch (url, init);
console. log ( " Response: " , response.status);
return response;
},
});
Making Requests
GET Requests
import { getTodo } from " @/contracts/todos " ;
// With path parameters
const todo = await apiClient
. endpoint (getTodo)
. call ({ path : { id : " 123 " } });
// With query parameters
const todos = await apiClient
. endpoint (listTodos)
. call ({
query : {
completed : false ,
limit : 10 ,
offset : 0 ,
},
});
POST Requests
import { createTodo } from " @/contracts/todos " ;
const newTodo = await apiClient
. endpoint (createTodo)
. call ({
body : {
title : " Learn Contract Kit " ,
description : " Read all the docs " ,
},
});
PUT/PATCH Requests
import { updateTodo } from " @/contracts/todos " ;
const updatedTodo = await apiClient
. endpoint (updateTodo)
. call ({
path : { id : " 123 " },
body : {
title : " Updated title " ,
completed : true ,
},
});
DELETE Requests
import { deleteTodo } from " @/contracts/todos " ;
await apiClient
. endpoint (deleteTodo)
. call ({ path : { id : " 123 " } });
Error Handling
ContractError
All client errors are instances of ContractError:
import { ContractError } from " @contract-kit/client " ;
try {
const todo = await apiClient
. endpoint (getTodo)
. call ({ path : { id : " 123 " } });
} catch (error) {
if (error instanceof ContractError ) {
console. log (error.status); // HTTP status code
console. log (error.details); // Error response body (validated)
console. log (error.message); // Error message
}
}
Handling Specific Error Codes
try {
const todo = await apiClient
. endpoint (getTodo)
. call ({ path : { id : " 123 " } });
} catch (error) {
if (error instanceof ContractError ) {
switch (error.status) {
case 404 :
console. log ( " Todo not found " );
break ;
case 401 :
console. log ( " Unauthorized " );
break ;
case 500 :
console. log ( " Server error " );
break ;
default :
console. log ( " Unknown error: " , error.status);
}
}
}
Type-Safe Error Details
When your contract defines error schemas, the error details are fully typed:
// Contract with error schema
const getTodo = todos
. get ( " /api/todos/:id " )
. path (z. object ({ id : z. string () }))
. response ( 200 , TodoSchema)
. errors ({
404 : z. object ({
code : z. literal ( " TODO_NOT_FOUND " ),
message : z. string (),
}),
});
// Error handling with typed details
try {
const todo = await apiClient. endpoint (getTodo). call ({ path : { id : " 123 " } });
} catch (error) {
if (error instanceof ContractError && error.status === 404 ) {
// error.details is typed as { code: "TODO_NOT_FOUND", message: string }
console. log (error.details.code); // "TODO_NOT_FOUND"
console. log (error.details.message); // string
}
}
Creating API Wrappers
Organize your API calls into reusable modules:
// features/todos/api.ts
import { apiClient } from " @/lib/api-client " ;
import { getTodo, createTodo, updateTodo, deleteTodo, listTodos } from " @/contracts/todos " ;
export const todosApi = {
get : ( id : string ) =>
apiClient. endpoint (getTodo). call ({ path : { id } }),
list : ( filters ?: { completed ?: boolean ; limit ?: number }) =>
apiClient. endpoint (listTodos). call ({ query : filters }),
create : ( data : { title : string ; description ?: string }) =>
apiClient. endpoint (createTodo). call ({ body : data }),
update : ( id : string , data : { title ?: string ; completed ?: boolean }) =>
apiClient. endpoint (updateTodo). call ({ path : { id }, body : data }),
delete : ( id : string ) =>
apiClient. endpoint (deleteTodo). call ({ path : { id } }),
};
// Usage
const todos = await todosApi. list ({ completed : false });
const newTodo = await todosApi. create ({ title : " New todo " });
React Integration
Use with React Query for powerful data fetching:
import { useQuery, useMutation } from " @tanstack/react-query " ;
import { todosApi } from " @/features/todos/api " ;
function TodosList () {
const { data, isLoading } = useQuery ({
queryKey : [ " todos " ],
queryFn : () => todosApi. list (),
});
const createMutation = useMutation ({
mutationFn : todosApi.create,
onSuccess : () => {
queryClient. invalidateQueries ({ queryKey : [ " todos " ] });
},
});
if (isLoading) return < div >Loading ...</ div > ;
return (
< div >
{ data ?. todos . map ( todo => (
< div key = {todo.id} > {todo.title} </ div >
))}
< button onClick = {() => createMutation.mutate({ title: " New todo " })} >
Add Todo
</ button >
</ div >
);
}
See @contract-kit/react-query for more powerful integrations.
API Reference
createClient(config)
Creates an HTTP client instance.
const client = createClient ({
baseUrl : string;
headers ?: () => Promise < Record < string, string>> | Record<string, string>;
fetch? : typeof fetch;
});
client.endpoint(contract)
Creates a typed endpoint for a contract.
const endpoint = client. endpoint (getTodo);
const result = await endpoint. call ({ path : { id : " 123 " } });
Type Exports
import { ContractError } from " @contract-kit/client " ;
import type {
CallArgs,
Client,
ClientConfig,
Endpoint,
EndpointCallArgs,
InferBody,
InferPathParams,
InferQuery,
InferSuccessResponse,
} from " @contract-kit/client " ;
Best Practices
Organize API calls into domain-specific modules (e.g., todosApi, usersApi) for better organization.
Handle errors consistently
Create a centralized error handling strategy using ContractError to handle common error codes.
Use dynamic headers for auth
Leverage TypeScript inference
Let TypeScript infer types from contracts instead of manually typing API responses.
Next Steps