Files
nextjs-elysia-allaos/docs/API_REFERENCE.md
phaichayon 043edff93a setup
2026-04-26 00:15:22 +07:00

22 KiB

API Reference for Front-end Developers

Last Updated: 2026-04-25
Version: 1.0.0


📋 Table of Contents

  1. Overview
  2. Authentication
  3. Base URL
  4. Response Format
  5. Error Handling
  6. Customers API
  7. Contacts API
  8. Contact Sharing API
  9. Type Definitions
  10. 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:

  1. Front-end authenticates with Keycloak
  2. Token is stored in window.__KEYCLOAK_TOKEN__
  3. API client automatically adds Authorization: Bearer {token} header
  4. 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:

  1. Created by the current user, OR
  2. Marked as public (isPublic: true), OR
  3. 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 ID
  • targetUserId (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:

  1. Check this documentation
  2. Check @/types/api.ts for type definitions
  3. Check contact-sharing-implementation-summary.md for contact sharing details
  4. Contact the backend team

End of API Reference