Skip to content

@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

ts
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

bash
bun add @ereo/client-sdk

Quick Start

ts
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

ts
function createClient(config?: ClientConfig): ApiClient

Configuration

ts
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

ts
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

ts
function configureClient(config: ClientConfig): void

Example

ts
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

ts
function getGlobalClient(): ApiClient

api

Type-safe API request helper using the global client.

Signature

ts
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

ts
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

MethodDescription
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

ts
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:

ts
// 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:

ts
// 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:

ts
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

ts
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

ts
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

ts
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:

ts
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:

ts
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

ts
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]:

ts
const { data } = await api('/api/posts/[id]').get({
  params: { id: '123' }
})

Request Headers

ts
const { data } = await api('/api/posts').get({
  headers: {
    'X-Custom-Header': 'value'
  }
})

Abort Signal

ts
const controller = new AbortController()

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000)

const { data } = await api('/api/posts').get({
  signal: controller.signal
})

FormData

ts
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

ts
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

ts
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

ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'

PathParams

Extracts path parameters from a route:

ts
type Params = PathParams<'/api/posts/[id]/comments/[commentId]'>
// { id: string; commentId: string }

MethodsFor

Gets all supported HTTP methods for a path:

ts
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:

ts
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:

ts
type PostsResponse = ResponseType<'/api/posts', 'GET'>
// Post[] (if defined in ApiRoutes)

RequestBody

Gets the request body type for a path and method:

ts
type CreatePostBody = RequestBody<'/api/posts', 'POST'>
// CreatePostInput (if defined in ApiRoutes)

Released under the MIT License.