22 KiB
API Reference for Front-end Developers
Last Updated: 2026-04-25
Version: 1.0.0
📋 Table of Contents
- Overview
- Authentication
- Base URL
- Response Format
- Error Handling
- Customers API
- Contacts API
- Contact Sharing API
- Type Definitions
- Usage Examples
Overview
This API provides type-safe access to the CRM system using ElysiaJS. All endpoints return consistent responses with built-in error handling.
Key Features:
- ✅ Type-safe with TypeScript
- ✅ Bearer token authentication via Keycloak
- ✅ Consistent response format
- ✅ Automatic error handling
- ✅ Multi-tenant support (branch-scoped)
Authentication
All API endpoints require authentication via Bearer token from Keycloak.
How it works:
- Front-end authenticates with Keycloak
- Token is stored in
window.__KEYCLOAK_TOKEN__ - API client automatically adds
Authorization: Bearer {token}header - Token refresh is handled automatically on 401 errors
Example:
// Token is automatically added by api-client.ts
import { apiClient } from "@/lib/api-client";
const response = await apiClient<CustomerListResponse>("/customers");
// Authorization header is added automatically
Base URL
Development: http://localhost:3000/api
Production: {TBD}
Response Format
Success Response
{
success: true,
data: T, // The actual data
message?: string, // Optional success message
count?: number // Optional count for list responses
}
Error Response
{
success: false,
error: string, // Error message
details?: string // Optional detailed error info
}
Error Handling
HTTP Status Codes
| Status | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request (invalid input) |
| 401 | Unauthorized (token expired/invalid) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found |
| 500 | Internal Server Error |
Error Response Structure
{
success: false,
error: "Customer not found or access denied",
details: "Customer ID 'xyz' does not exist"
}
Handling Errors in Front-end
try {
const response = await apiClient<CustomerResponse>("/customers/123");
if (!response.success) {
// Handle error
console.error(response.error);
return;
}
// Success - use response.data
console.log(response.data);
} catch (error) {
// Network error or unexpected error
console.error("API call failed:", error);
}
Customers API
1. Get All Customers
Get all customers for the current user's branch.
Endpoint: GET /customers
Query Parameters:
status(optional) - Filter by status:active,inactive,pending
Request:
GET /customers
GET /customers?status=active
Response:
{
success: true,
data: Customer[],
count: 10,
message: "Found 10 customer(s)"
}
Example:
import { apiClient } from "@/lib/api-client";
import type { CustomerListResponse } from "@/types/api";
const response = await apiClient<CustomerListResponse>(
"/customers?status=active",
);
2. Get Single Customer
Get a specific customer by ID.
Endpoint: GET /customers/:id
Path Parameters:
id(required) - Customer ID
Request:
GET /customers/123e4567-e89b-12d3-a456-426614174000
Response:
{
success: true,
data: Customer
}
Example:
const response = await apiClient<CustomerResponse>(`/customers/${customerId}`);
3. Create Customer
Create a new customer.
Endpoint: POST /customers
Request Body:
{
name: string, // Required
email: string, // Required (must be valid email)
phone: string, // Required
company: string, // Required
address: string, // Required
customerStatus?: string, // Optional
customerType?: string, // Optional
taxId?: string // Optional
}
Response:
{
success: true,
data: Customer,
message: "Customer created successfully"
}
Example:
import type { CreateCustomerRequest } from "@/types/api";
const newCustomer: CreateCustomerRequest = {
name: "สมชาย ใจดี",
email: "somchai@example.com",
phone: "081-234-5678",
company: "บริษัท ไทยธุรกิจ จำกัด",
address: "123 ถนนสุขุมวิท แขวงคลองตัน เขตคลองเตย กรุงเทพฯ 10110",
customerStatus: "active",
customerType: "corporate",
taxId: "0105551234567",
};
const response = await apiClient<CreateCustomerResponse>("/customers", {
method: "POST",
body: JSON.stringify(newCustomer),
});
4. Update Customer
Update an existing customer.
Endpoint: PUT /customers/:id
Path Parameters:
id(required) - Customer ID
Request Body:
{
name?: string,
email?: string,
phone?: string,
company?: string,
address?: string,
customerStatus?: string,
erpCustomerCode?: string // For ERP sync
}
Response:
{
success: true,
data: Customer,
message: "Customer updated successfully"
}
Example:
const updates = {
erpCustomerCode: "ERP-001234",
};
const response = await apiClient<UpdateCustomerResponse>(
`/customers/${customerId}`,
{
method: "PUT",
body: JSON.stringify(updates),
},
);
5. Delete Customer
Delete a customer (soft delete).
Endpoint: DELETE /customers/:id
Path Parameters:
id(required) - Customer ID
Response:
{
success: true,
data: Customer,
message: "Customer deleted successfully"
}
Example:
const response = await apiClient<DeleteCustomerResponse>(
`/customers/${customerId}`,
{
method: "DELETE",
},
);
Contacts API
1. Get Contacts for Customer
Get all visible contacts for a customer.
Endpoint: GET /customers/:customerId/contacts
Path Parameters:
customerId(required) - Customer ID
Visibility Rules: A contact is visible if:
- Created by the current user, OR
- Marked as public (
isPublic: true), OR - Shared with the current user via
contact_shares
Response:
{
success: true,
data: Contact[],
count: 5,
message: "Found 5 contact(s)"
}
Example:
const response = await apiClient<ContactListResponse>(
`/customers/${customerId}/contacts`,
);
2. Create Contact
Create a new contact for a customer.
Endpoint: POST /customers/:customerId/contacts
Path Parameters:
customerId(required) - Customer ID
Request Body:
{
name: string, // Required
position?: string, // Optional
phone?: string, // Optional
mobile?: string, // Optional
email?: string, // Optional
isPrimary?: boolean, // Optional (default: false)
notes?: string // Optional
}
Response:
{
success: true,
data: Contact,
message: "Contact created successfully"
}
Example:
const newContact: CreateContactRequest = {
name: "วิภาวี สุขสันต์",
position: "ผู้จัดการฝ่ายจัดซื้อ",
phone: "02-123-4567",
mobile: "089-876-5432",
email: "wipavi@example.com",
isPrimary: true,
notes: "Key decision maker",
};
const response = await apiClient<CreateContactResponse>(
`/customers/${customerId}/contacts`,
{
method: "POST",
body: JSON.stringify(newContact),
},
);
3. Update Contact
Update an existing contact.
Endpoint: PUT /contacts/:contactId
Path Parameters:
contactId(required) - Contact ID
Rules:
- Only the contact creator can update
Request Body:
{
name?: string,
position?: string,
phone?: string,
mobile?: string,
email?: string,
isPrimary?: boolean,
isPublic?: boolean,
notes?: string
}
Response:
{
success: true,
data: Contact,
message: "Contact updated successfully"
}
Example:
const updates = {
isPublic: true,
notes: "Shared with team",
};
const response = await apiClient<UpdateContactResponse>(
`/contacts/${contactId}`,
{
method: "PUT",
body: JSON.stringify(updates),
},
);
4. Share Contact (Make Public)
Make a contact visible to all users in the branch.
Endpoint: POST /contacts/:contactId/share
Path Parameters:
contactId(required) - Contact ID
Rules:
- Only the contact creator can share
Response:
{
success: true,
data: Contact,
message: "Contact shared successfully"
}
Example:
const response = await apiClient<ShareContactResponse>(
`/contacts/${contactId}/share`,
{
method: "POST",
},
);
5. Unshare Contact (Make Private)
Make a contact private (visible only to creator).
Endpoint: POST /contacts/:contactId/unshare
Path Parameters:
contactId(required) - Contact ID
Rules:
- Only the contact creator can unshare
Response:
{
success: true,
data: Contact,
message: "Contact unshared successfully"
}
Example:
const response = await apiClient<UnshareContactResponse>(
`/contacts/${contactId}/unshare`,
{
method: "POST",
},
);
6. Delete Contact
Delete a contact.
Endpoint: DELETE /contacts/:contactId
Path Parameters:
contactId(required) - Contact ID
Rules:
- Only the contact creator can delete
Response:
{
success: true,
data: Contact,
message: "Contact deleted successfully"
}
Example:
const response = await apiClient<DeleteContactResponse>(
`/contacts/${contactId}`,
{
method: "DELETE",
},
);
Contact Sharing API
1. Share Contact with Specific User
Share a contact with a specific user (not public).
Endpoint: POST /contacts/:contactId/share-with
Path Parameters:
contactId(required) - Contact ID
Request Body:
{
targetUserId: string, // Required - User ID to share with
notes?: string // Optional - Notes about the share
}
Rules:
- Only the contact creator can share
- Cannot share with yourself
- Cannot share non-existent contact
- Cannot share contact from different branch
- Duplicate share prevention
Response:
{
success: true,
data: ContactShare,
message: "Contact shared successfully"
}
Example:
const shareRequest = {
targetUserId: "user-456",
notes: "Sales lead for Q4 project",
};
const response = await apiClient<ShareContactWithUserResponse>(
`/contacts/${contactId}/share-with`,
{
method: "POST",
body: JSON.stringify(shareRequest),
},
);
2. Unshare Contact from Specific User
Remove sharing from a specific user.
Endpoint: DELETE /contacts/:contactId/share/:targetUserId
Path Parameters:
contactId(required) - Contact IDtargetUserId(required) - User ID to unshare from
Rules:
- Only the contact creator can unshare
Response:
{
success: true,
data: ContactShare,
message: "Contact unshared successfully"
}
Example:
const response = await apiClient<UnshareContactFromUserResponse>(
`/contacts/${contactId}/share/${targetUserId}`,
{
method: "DELETE",
},
);
3. Get Contact Shares
Get all shares for a contact.
Endpoint: GET /contacts/:contactId/shares
Path Parameters:
contactId(required) - Contact ID
Rules:
- Only the contact creator can view shares
Response:
{
success: true,
data: ContactShare[],
count: 3,
message: "Found 3 share(s)"
}
Example:
const response = await apiClient<ContactShareListResponse>(
`/contacts/${contactId}/shares`,
);
4. Get Contacts Shared With Me
Get all contacts that have been shared with the current user.
Endpoint: GET /contacts/shared-with-me
Query Parameters:
customerId(optional) - Filter by customer ID
Response:
{
success: true,
data: Contact[],
count: 5,
message: "Found 5 contact(s) shared with you"
}
Example:
// Get all contacts shared with me
const response1 = await apiClient<ContactListResponse>(
"/contacts/shared-with-me",
);
// Filter by customer
const response2 = await apiClient<ContactListResponse>(
`/contacts/shared-with-me?customerId=${customerId}`,
);
Type Definitions
All API types are exported from @/types/api.
Import Example:
import type {
Customer,
Contact,
ContactShare,
CustomerListResponse,
CreateCustomerRequest,
UpdateContactRequest,
ApiResponse,
} from "@/types/api";
Available Types:
- Customer Types:
Customer,CreateCustomerRequest,UpdateCustomerRequest - Contact Types:
Contact,CreateContactRequest,UpdateContactRequest - Share Types:
ContactShare,ShareContactRequest - Response Types:
SuccessResponse<T>,ErrorResponse,ApiResponse<T> - List Responses:
CustomerListResponse,ContactListResponse,ContactShareListResponse - Single Item Responses:
CustomerResponse,ContactResponse,ContactShareResponse - Operation Responses:
CreateCustomerResponse,UpdateCustomerResponse, etc.
Usage Examples
Example 1: Fetch and Display Customers
"use client";
import { useEffect, useState } from "react";
import { apiClient } from "@/lib/api-client";
import type { CustomerListResponse, Customer } from "@/types/api";
export default function CustomerList() {
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchCustomers() {
try {
const response = await apiClient<CustomerListResponse>("/customers?status=active");
if (response.success) {
setCustomers(response.data);
} else {
setError(response.error);
}
} catch (err) {
setError("Failed to fetch customers");
} finally {
setLoading(false);
}
}
fetchCustomers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{customers.map(customer => (
<li key={customer.id}>
{customer.name} - {customer.company}
</li>
))}
</ul>
);
}
Example 2: Create New Customer with Form
"use client";
import { useState } from "react";
import { apiClient } from "@/lib/api-client";
import type { CreateCustomerRequest, CreateCustomerResponse } from "@/types/api";
export default function CreateCustomerForm() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);
const formData = new FormData(e.currentTarget);
const request: CreateCustomerRequest = {
name: formData.get("name") as string,
email: formData.get("email") as string,
phone: formData.get("phone") as string,
company: formData.get("company") as string,
address: formData.get("address") as string,
customerStatus: formData.get("status") as string || undefined
};
try {
const response = await apiClient<CreateCustomerResponse>("/customers", {
method: "POST",
body: JSON.stringify(request)
});
if (response.success) {
setSuccess(true);
e.currentTarget.reset();
} else {
setError(response.error);
}
} catch (err) {
setError("Failed to create customer");
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<input name="phone" placeholder="Phone" required />
<input name="company" placeholder="Company" required />
<input name="address" placeholder="Address" required />
<select name="status">
<option value="">Select status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Customer"}
</button>
{error && <div className="error">{error}</div>}
{success && <div className="success">Customer created successfully!</div>}
</form>
);
}
Example 3: Share Contact with User
"use client";
import { useState } from "react";
import { apiClient } from "@/lib/api-client";
import type { ShareContactWithUserResponse } from "@/types/api";
interface ShareContactModalProps {
contactId: string;
onClose: () => void;
}
export default function ShareContactModal({ contactId, onClose }: ShareContactModalProps) {
const [targetUserId, setTargetUserId] = useState("");
const [notes, setNotes] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleShare() {
if (!targetUserId) {
setError("Please select a user");
return;
}
setLoading(true);
setError(null);
try {
const response = await apiClient<ShareContactWithUserResponse>(
`/contacts/${contactId}/share-with`,
{
method: "POST",
body: JSON.stringify({ targetUserId, notes })
}
);
if (response.success) {
onClose(); // Close modal on success
} else {
setError(response.error);
}
} catch (err) {
setError("Failed to share contact");
} finally {
setLoading(false);
}
}
return (
<div className="modal">
<h2>Share Contact</h2>
<input
type="text"
placeholder="User ID"
value={targetUserId}
onChange={(e) => setTargetUserId(e.target.value)}
/>
<textarea
placeholder="Notes (optional)"
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
<button onClick={handleShare} disabled={loading}>
{loading ? "Sharing..." : "Share"}
</button>
<button onClick={onClose}>Cancel</button>
{error && <div className="error">{error}</div>}
</div>
);
}
Example 4: Get Contacts Shared With Me
"use client";
import { useEffect, useState } from "react";
import { apiClient } from "@/lib/api-client";
import type { ContactListResponse, Contact } from "@/types/api";
export default function SharedContactsList() {
const [contacts, setContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchSharedContacts() {
try {
const response = await apiClient<ContactListResponse>("/contacts/shared-with-me");
if (response.success) {
setContacts(response.data);
}
} catch (err) {
console.error("Failed to fetch shared contacts");
} finally {
setLoading(false);
}
}
fetchSharedContacts();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>Contacts Shared With Me</h2>
{contacts.length === 0 ? (
<p>No contacts shared with you yet.</p>
) : (
<ul>
{contacts.map(contact => (
<li key={contact.id}>
<strong>{contact.name}</strong>
{contact.position && <span> - {contact.position}</span>}
{contact.email && <span> ({contact.email})</span>}
<br />
<small>Shared by user</small>
</li>
))}
</ul>
)}
</div>
);
}
Best Practices
1. Always Handle Errors
try {
const response = await apiClient<SomeResponse>("/endpoint");
if (!response.success) {
// Handle API error
console.error(response.error);
return;
}
// Success
} catch (error) {
// Handle network error
console.error("Network error:", error);
}
2. Use Type Guards
function isSuccess<T>(
response: ApiResponse<T>,
): response is SuccessResponse<T> {
return response.success === true;
}
if (isSuccess(response)) {
// TypeScript knows response is SuccessResponse
console.log(response.data);
}
3. Cache Data with React Query
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/lib/api-client";
import type { CustomerListResponse } from "@/types/api";
function useCustomers(status?: string) {
return useQuery({
queryKey: ["customers", status],
queryFn: () =>
apiClient<CustomerListResponse>(
`/customers${status ? `?status=${status}` : ""}`,
),
});
}
4. Optimistic Updates
import { useMutation, useQueryClient } from "@tanstack/react-query";
function useUpdateCustomer() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateCustomerRequest }) =>
apiClient<UpdateCustomerResponse>(`/customers/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["customers"] });
},
});
}
Support
For questions or issues:
- Check this documentation
- Check
@/types/api.tsfor type definitions - Check
contact-sharing-implementation-summary.mdfor contact sharing details - Contact the backend team
End of API Reference