@ereo/client-sdk
Type-safe client SDK for making API calls to EreoJS applications. Provides end-to-end type safety from your API routes to your client code.
Import
import {
ApiClient,
createClient,
getGlobalClient,
configureClient,
api
} from '@ereo/client-sdk'Overview
The @ereo/client-sdk package provides a fully type-safe HTTP client for consuming EreoJS API routes. When used with TypeScript, it offers autocomplete for routes, type checking for request bodies and query parameters, and typed responses.
Installation
bun add @ereo/client-sdkQuick Start
import { api } from '@ereo/client-sdk'
// Make type-safe API calls
const { data: posts } = await api('/api/posts').get()
const { data: post } = await api('/api/posts').post({ body: { title: 'Hello' } })API Reference
createClient
Creates a new API client instance with custom configuration.
Signature
function createClient(config?: ClientConfig): ApiClientConfiguration
interface ClientConfig {
// Base URL for API requests
baseUrl?: string
// Default headers for all requests
headers?: Record<string, string>
// Request timeout in milliseconds (default: 30000)
timeout?: number
// Enable request/response logging
debug?: boolean
// Custom fetch implementation
fetch?: typeof fetch
// Request interceptor
onRequest?: (config: ApiRequestConfig) => ApiRequestConfig | Promise<ApiRequestConfig>
// Response interceptor
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>
// Error handler
onError?: (error: ApiError) => void | Promise<void>
}Example
import { createClient } from '@ereo/client-sdk'
const client = createClient({
baseUrl: 'https://api.example.com',
headers: {
'Authorization': `Bearer ${token}`
},
timeout: 10000,
debug: process.env.NODE_ENV === 'development'
})configureClient
Configures the global API client.
Signature
function configureClient(config: ClientConfig): voidExample
import { configureClient } from '@ereo/client-sdk'
// Configure once at app startup
configureClient({
baseUrl: '/api',
headers: {
'X-API-Version': '1'
}
})getGlobalClient
Gets the global API client instance, creating one if it does not exist.
Signature
function getGlobalClient(): ApiClientapi
Type-safe API request helper using the global client.
Signature
function api<Path extends ApiPaths>(path: Path): {
get: (options?) => Promise<ApiResponse<ResponseType<Path, 'GET'>>>
post: (options?) => Promise<ApiResponse<ResponseType<Path, 'POST'>>>
put: (options?) => Promise<ApiResponse<ResponseType<Path, 'PUT'>>>
patch: (options?) => Promise<ApiResponse<ResponseType<Path, 'PATCH'>>>
delete: (options?) => Promise<ApiResponse<ResponseType<Path, 'DELETE'>>>
}Example
import { api } from '@ereo/client-sdk'
// GET request
const { data: users } = await api('/api/users').get()
// GET with query parameters
const { data: filteredUsers } = await api('/api/users').get({
query: { role: 'admin', limit: 10 }
})
// POST with body
const { data: newUser } = await api('/api/users').post({
body: { name: 'John', email: 'john@example.com' }
})
// PUT with path parameters
const { data: updatedUser } = await api('/api/users/[id]').put({
params: { id: '123' },
body: { name: 'John Updated' }
})
// DELETE
await api('/api/users/[id]').delete({
params: { id: '123' }
})ApiClient Class
The main client class for making HTTP requests.
Methods
| Method | Description |
|---|---|
configure(config) | Update client configuration |
request(config) | Make a generic request |
get(path, options?) | Make a GET request |
post(path, options?) | Make a POST request |
put(path, options?) | Make a PUT request |
patch(path, options?) | Make a PATCH request |
delete(path, options?) | Make a DELETE request |
Example
import { createClient } from '@ereo/client-sdk'
const client = createClient({ baseUrl: '/api' })
// Using convenience methods
const users = await client.get('/users')
const newUser = await client.post('/users', { body: { name: 'John' } })
// Using generic request
const result = await client.request({
path: '/users',
method: 'GET',
query: { limit: 10 }
})Type Safety
Defining API Types
API types can be defined directly in your route files:
// app/routes/api/posts.ts
import type { DefineApiTypes } from '@ereo/client-sdk'
interface Post {
id: string
title: string
content: string
}
interface CreatePostInput {
title: string
content: string
}
export interface ApiTypes extends DefineApiTypes<{
GET: {
response: Post[]
query: { limit?: number; offset?: number }
}
POST: {
body: CreatePostInput
response: Post
}
}> {}
export async function GET({ query }) {
// query is typed as { limit?: number; offset?: number }
return { posts: await getPosts(query) }
}
export async function POST({ body }) {
// body is typed as CreatePostInput
return { post: await createPost(body) }
}Module Augmentation
For full end-to-end type safety, augment the ApiRoutes interface:
// types/api.d.ts
declare module '@ereo/client-sdk' {
interface ApiRoutes {
'/api/posts': {
GET: {
response: Post[]
query?: { limit?: number; offset?: number }
}
POST: {
body: CreatePostInput
response: Post
}
}
'/api/posts/[id]': {
GET: {
response: Post
}
PUT: {
body: Partial<CreatePostInput>
response: Post
}
DELETE: {
response: { success: boolean }
}
}
}
}Now your API calls are fully typed:
import { api } from '@ereo/client-sdk'
// Autocomplete for paths
const { data } = await api('/api/posts').get({
query: { limit: 10 } // Type checked
})
// data is typed as Post[]
console.log(data[0].title)Error Handling
ApiError Interface
interface ApiError extends Error {
status: number // HTTP status code
data?: unknown // Response data (if any)
path: string // Request path
method: string // Request method
}Handling Errors
import { api } from '@ereo/client-sdk'
try {
const { data } = await api('/api/posts').get()
} catch (error) {
if (error.status === 404) {
console.log('Resource not found')
} else if (error.status === 401) {
// Redirect to login
} else {
console.error('API Error:', error.message)
}
}Global Error Handler
import { configureClient } from '@ereo/client-sdk'
configureClient({
onError: async (error) => {
if (error.status === 401) {
// Refresh token or redirect to login
await refreshAuth()
}
// Log to error tracking service
trackError(error)
}
})Interceptors
Request Interceptor
Modify requests before they are sent:
import { configureClient } from '@ereo/client-sdk'
configureClient({
onRequest: async (config) => {
// Add auth token to every request
const token = await getAuthToken()
return {
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${token}`
}
}
}
})Response Interceptor
Transform responses before they are returned:
import { configureClient } from '@ereo/client-sdk'
configureClient({
onResponse: async (response) => {
// Log all responses
console.log(`[${response.status}] ${response.ok ? 'OK' : 'Error'}`)
// Transform data
return {
...response,
data: camelCaseKeys(response.data)
}
}
})Request Options
Query Parameters
const { data } = await api('/api/posts').get({
query: {
page: 1,
limit: 20,
sort: 'createdAt',
order: 'desc'
}
})Path Parameters
For dynamic routes like /api/posts/[id]:
const { data } = await api('/api/posts/[id]').get({
params: { id: '123' }
})Request Headers
const { data } = await api('/api/posts').get({
headers: {
'X-Custom-Header': 'value'
}
})Abort Signal
const controller = new AbortController()
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)
const { data } = await api('/api/posts').get({
signal: controller.signal
})FormData
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('name', 'My File')
const { data } = await api('/api/upload').post({
body: formData
})Response Structure
ApiResponse Interface
interface ApiResponse<T = unknown> {
data: T // Response data
status: number // HTTP status code
headers: Headers // Response headers
ok: boolean // Whether response was successful (2xx)
}Example
const response = await api('/api/posts').get()
console.log(response.data) // Post[]
console.log(response.status) // 200
console.log(response.ok) // true
console.log(response.headers.get('x-total-count'))Type Helpers
HttpMethod
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'PathParams
Extracts path parameters from a route:
type Params = PathParams<'/api/posts/[id]/comments/[commentId]'>
// { id: string; commentId: string }MethodsFor
Gets all supported HTTP methods for a path:
type PostMethods = MethodsFor<'/api/posts'>
// 'GET' | 'POST' (if defined in ApiRoutes)
// For unregistered paths, returns all HTTP methods
type UnknownMethods = MethodsFor<'/unknown'>
// HttpMethod ('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS')QueryParams
Gets the query parameters type for a path and method:
type PostsQuery = QueryParams<'/api/posts', 'GET'>
// { limit?: number; offset?: number } (if defined in ApiRoutes)
// For unregistered paths, returns a generic record type
type UnknownQuery = QueryParams<'/unknown', 'GET'>
// Record<string, string | number | boolean | undefined>ResponseType
Gets the response type for a path and method:
type PostsResponse = ResponseType<'/api/posts', 'GET'>
// Post[] (if defined in ApiRoutes)RequestBody
Gets the request body type for a path and method:
type CreatePostBody = RequestBody<'/api/posts', 'POST'>
// CreatePostInput (if defined in ApiRoutes)