@tcnext/tcapi-client
v1.0.3
Published
`tcAPIClient` is a modular and extensible API client for TypeScript projects, built on top of Axios. It's designed to simplify interactions with REST APIs by providing a clean, intuitive interface for common operations, along with advanced features like c
Readme
tcAPI Client
tcAPIClient is a modular and extensible API client for TypeScript projects, built on top of Axios. It's designed to simplify interactions with REST APIs by providing a clean, intuitive interface for common operations, along with advanced features like collection-based CRUD, real-time event handling, and a query builder.
Features
- Axios-Powered: Leverages the robustness and flexibility of Axios for HTTP requests.
- Typed Collections: Provides a type-safe way to interact with API resources (collections).
- CRUD Operations: Simplifies common Create, Read, Update, and Delete operations on collections.
- Query Builder Integration: Allows for complex querying with filtering, sorting, and pagination. (Assumes a
query-builder.tsutility is present). - Real-time Event System: Enables event-driven updates for API requests, allowing your application to react to data changes.
- Direct HTTP Methods: Offers direct access to GET, POST, PATCH, PUT, DELETE, HEAD, and OPTIONS methods for more granular control.
- Configurable: Supports global configuration for
baseURL, authentication tokens, default pagination limits, and Axios-specific settings. - Request Logging: Optional logging of API calls for debugging purposes.
Installation
This client is intended to be used as part of a project. Ensure you have the necessary dependencies, primarily axios.
If you're using this in a project managed by Bun (as indicated by the original project setup):
bun install axios
# or if you prefer npm
# npm install axios
# or yarn
# yarn add axiosGetting Started
Initialization
First, import and instantiate the tcapiClient:
import { tcapiClient } from "./src/tcapi-client"; // Adjust path as needed
// Define an interface for your data (optional but recommended)
interface Article {
id: string | number;
title: string;
content: string;
// other fields...
}
const apiClient = new tcapiClient({
baseURL: "https://api.example.com/v1", // Your API base URL
auth: "YOUR_API_TOKEN", // Optional: Bearer token for authentication
defaultPaginationLimit: 25, // Optional: Default limit for paginated collection queries
logCalls: true, // Optional: Set to true to log API requests to the console
axiosConfig: {
// Optional: Any additional Axios request config defaults
timeout: 5000
}
});Constructor Options (TcApiConstructorProps)
baseURL?: string: The base URL for all API requests.auth?: string: An authentication token. If provided, it will be sent as a Bearer token in theAuthorizationheader.axiosConfig?: AxiosRequestConfig: Additional Axios configuration options to be applied to the underlying Axios instance.defaultPaginationLimit?: number: Sets a defaultlimitfor pagination when using collection methods if not specified in the query.logCalls?: boolean: Iftrue, the client will log details (method and URL) of each API call to the console. Defaults tofalse.
Usage
1. Collection-based Operations
The collection<TItem>(collectionName: string) method provides a handler for a specific API resource (e.g., "articles", "users"). TItem is the expected type for items in that collection.
const articlesCollection = apiClient.collection<Article>("articles");
// --- Find multiple items ---
async function fetchAllArticles() {
try {
const response = await articlesCollection.find();
// Default pagination limit (if set) will be applied here
// response.data will be Article[]
console.log("All articles:", response.data);
} catch (error) {
console.error("Error fetching articles:", error);
}
}
// --- Find with Query Parameters (Sorting, Filtering, Pagination) ---
async function fetchRecentArticles() {
try {
const response = await articlesCollection.find({
sort: [{ field: "createdAt", order: "desc" }],
filters: [{ field: "status", operator: "$eq", value: "published" }],
pagination: { limit: 5, offset: 0 } // Overrides defaultPaginationLimit
});
console.log("Recent articles:", response.data);
} catch (error) {
console.error("Error fetching recent articles:", error);
}
}
// --- Find a single item by ID ---
async function fetchArticleById(id: string | number) {
try {
const response = await articlesCollection.findOne(id);
// response.data will be Article
console.log("Article:", response.data);
} catch (error) {
console.error(`Error fetching article ${id}:`, error);
}
}
// --- Create a new item ---
async function createArticle(newArticleData: Partial<Article>) {
try {
const response = await articlesCollection.create(newArticleData);
// response.data will be the created Article
console.log("Created article:", response.data);
return response.data;
} catch (error) {
console.error("Error creating article:", error);
}
}
// --- Update an existing item ---
async function updateArticle(id: string | number, updates: Partial<Article>) {
try {
const response = await articlesCollection.update(id, updates);
// response.data will be the updated Article
console.log("Updated article:", response.data);
} catch (error) {
console.error(`Error updating article ${id}:`, error);
}
}
// --- Delete an item ---
async function deleteArticle(id: string | number) {
try {
const response = await articlesCollection.delete(id);
// response.data might be the deleted Article or a confirmation, depends on API
console.log("Deleted article:", response.data);
} catch (error) {
console.error(`Error deleting article ${id}:`, error);
}
}2. Query Builder (QueryBuilderSettings)
Collection methods (find, findOne, create, update, delete) accept an optional queryParams object. This object is processed by the queryBuilder utility to construct the query string.
The structure of QueryBuilderSettings (defined in query-builder.ts) typically includes:
sort?: { field: string; order: "asc" | "desc" }[]: Sorting criteria.filters?: { field: string; operator: string; value: any }[]: Filtering conditions.pagination?: { limit?: number; offset?: number; page?: number; pageSize?: number }: Pagination parameters.- Other custom query parameters your API might support.
Note: The defaultPaginationLimit set during client initialization will be automatically applied if queryParams.pagination.limit is not provided in a collection method call.
3. Real-time Updates with Events
The client allows you to listen for events and re-fetch data when those events are emitted. This is useful for keeping your UI in sync with backend changes without manual polling.
Each request made through a CollectionHandler or a direct HTTP method (that returns TcApiRequest) has an .on() method.
const postsCollection = apiClient.collection<Article>("posts");
// 1. Initial fetch and register an event listener
postsCollection
.find({ sort: [{ field: "date", order: "desc" }] })
.on("postsUpdated", (response) => {
// This callback is triggered when 'postsUpdated' event is emitted
console.log("🔄 Posts updated (event):", response.data);
// Update your UI or state here with response.data
})
.then((response) => {
// This is the response from the initial .find() call
console.log("📰 Initial posts:", response.data);
// Populate your UI or state with initial data
})
.catch((error) => {
console.error("❌ Error fetching posts:", error);
});
// 2. Somewhere else in your application, when you know data has changed:
function someActionThatChangesPosts() {
// After an action like creating or updating a post on the server...
console.log("🚀 Emitting 'postsUpdated' event...");
apiClient.emit("postsUpdated");
}
// Example: Trigger the event after 5 seconds
setTimeout(someActionThatChangesPosts, 5000);How it works:
- When you call
.on("eventName", callback), the original request function is registered with the client under "eventName". - The initial request executes as usual.
- When
client.emit("eventName")is called, the client re-executes the registered request function. - The
onSuccesshandler (which wraps youreventCallback) is then called with the new data, and the promise chain associated with the.on()call is resolved/rejected.
4. Direct HTTP Methods
For requests that don't fit the collection model or for more direct control, you can use Axios-like methods directly on the client instance. These methods also return a TcApiRequest object, so they support the .on() event mechanism.
// --- GET Request ---
async function fetchUserData(userId: string) {
try {
const response = await apiClient.get<{ id: string; name: string }>(
`/users/${userId}`
);
console.log("User data:", response.data);
} catch (error) {
console.error("Error fetching user data:", error);
}
}
// --- POST Request ---
async function createUser(userData: { name: string; email: string }) {
try {
const response = await apiClient.post<{ id: string; createdAt: string }>(
"/users",
userData
);
console.log("Created user:", response.data);
} catch (error) {
console.error("Error creating user:", error);
}
}
// --- PATCH Request ---
async function updateUserProfile(
userId: string,
profileData: { bio?: string }
) {
try {
const response = await apiClient.patch(`/users/${userId}`, profileData);
console.log("Updated profile:", response.data);
} catch (error) {
console.error("Error updating profile:", error);
}
}
// --- PUT Request --- (Note: PATCH is generally preferred for partial updates)
async function replaceUser(
userId: string,
fullUserData: { name: string; email: string }
) {
try {
const response = await apiClient.put(`/users/${userId}`, fullUserData);
console.log("Replaced user:", response.data);
} catch (error) {
console.error("Error replacing user:", error);
}
}
// --- DELETE Request ---
async function deleteUser(userId: string) {
try {
const response = await apiClient.delete(`/users/${userId}`);
console.log("Delete response:", response.data); // Or check response.status
} catch (error) {
console.error("Error deleting user:", error);
}
}
// --- HEAD Request ---
async function checkResourceHeaders(url: string) {
try {
const response = await apiClient.head(url); // Returns AxiosResponse, not TcApiRequest
console.log("Headers:", response.headers);
} catch (error) {
console.error("Error with HEAD request:", error);
}
}
// --- OPTIONS Request ---
async function checkSupportedMethods(url: string) {
try {
const response = await apiClient.options(url); // Returns AxiosResponse, not TcApiRequest
console.log("Allowed methods:", response.headers.allow);
} catch (error) {
console.error("Error with OPTIONS request:", error);
}
}
// --- Custom Request ---
async function makeCustomRequest() {
try {
const response = await apiClient.request({
// Returns AxiosResponse, not TcApiRequest
method: "GET",
url: "/some-custom-endpoint",
params: {
customParam: "value"
}
});
console.log("Custom request response:", response.data);
} catch (error) {
console.error("Error with custom request:", error);
}
}Note on TcApiRequest: Most methods (get, post, patch, put, delete from the client, and all CollectionHandler methods) return a TcApiRequest. This is an extended Promise that includes the .on() method for event handling. head, options, and request methods return a standard Axios Promise<AxiosResponse>.
5. Accessing the Axios Instance
If you need to interact with the underlying Axios instance directly (e.g., to add interceptors dynamically after client initialization or use features not exposed by tcapiClient):
const axiosInstance = apiClient.getInstance();
// Example: Add a response interceptor
axiosInstance.interceptors.response.use(
(response) => {
// Any status code that lie within the range of 2xx cause this function to trigger
return response;
},
(error) => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
console.error("Global Axios error interceptor:", error.message);
return Promise.reject(error);
}
);This README.md provides a comprehensive guide to using the tcapiClient. Remember to adapt paths and type definitions (Article in examples) to your specific project structure and data models.
