npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clinchdate/api-client

v2.0.0

Published

Type-safe API client library for ClinchDate - works across mobile, web, and admin applications

Readme

ClinchDate API Client SDK — Complete Technical Documentation

Version: 1.0.2
Last Updated: 01 March 2026
Audience: Frontend Engineers, Mobile Developers, Full-Stack Developers, Technical Leads
Platform: ClinchDate — Multi-platform dating app targeting Cameroon, West Africa & the World


Table of Contents

  1. Executive Summary
  2. Architecture Overview
  3. What Is the API Client SDK?
  4. Core Design Principles
  5. Package Structure & File Breakdown
  6. Initialization & Configuration System
  7. HTTP Layer — Axios Instance Manager
  8. Authentication Interceptor System
  9. Service Layer — Detailed Breakdown
  10. React Hooks Layer
  11. Type System & Contracts
  12. Error Handling Architecture
  13. Socket.IO Real-Time Layer
  14. Integration Guide — Web App (React/Vite)
  15. Integration Guide — Mobile App (React Native/Expo)
  16. Integration Guide — Admin Panel
  17. Environment Configuration
  18. Caching Strategy
  19. Testing Strategy
  20. Security Considerations
  21. Performance Optimization
  22. Troubleshooting & Common Issues
  23. API Routes Reference
  24. Deployment & Publishing
  25. Changelog & Versioning

1. Executive Summary

The @clinchdate/api-client is a production-grade, type-safe TypeScript SDK that serves as the single communication bridge between all ClinchDate frontend applications (mobile, web, admin) and the backend API. Rather than each frontend implementing its own HTTP logic, authentication handling, and error management, the SDK consolidates all of this into a single, rigorously tested npm package.

This means any engineer working on any ClinchDate frontend writes zero HTTP boilerplate. They import a service, call a method, and receive typed data. Authentication, token refresh, error normalization, retry logic, and request/response logging are all handled transparently by the SDK.

Why this matters for ClinchDate:

  • Consistency: Every frontend speaks to the backend identically. A bug fix in the SDK propagates to all platforms with a single version bump.
  • Speed: New features on any frontend require only calling a new service method — no endpoint wiring, no auth plumbing.
  • Safety: TypeScript contracts ensure that if the backend changes a response shape, compilation fails in every consuming app before it reaches production.
  • Scalability: Adding a sixth frontend (desktop app, Chrome extension, partner integration) requires only installing the SDK — the entire API surface is immediately available.

2. Architecture Overview

ClinchDate operates on a polyrepo architecture — five independent codebases that converge through the shared API Client SDK.

┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
│   Mobile App    │   │     Web App     │   │   Admin Panel   │
│  React Native   │   │   React/Vite    │   │   React/Vite    │
│   Expo Router   │   │                 │   │                 │
└────────┬────────┘   └────────┬────────┘   └────────┬────────┘
         │                     │                     │
         └─────────────┬───────┴─────────────────────┘
                       │
              ┌────────▼─────────┐
              │  @clinchdate/    │
              │  api-client SDK  │    ← Single source of truth
              │  (npm package)   │       for all API communication
              └────────┬─────────┘
                       │
              ┌────────▼─────────┐
              │   Backend API    │
              │  Node.js/Express │
              │  TypeScript      │
              │  MongoDB Atlas   │
              └────────┬─────────┘
                       │
         ┌─────────────┼──────────────────┐
         │             │                  │
    ┌────▼───┐   ┌─────▼──────┐   ┌──────▼─────┐
    │ Twilio │   │   Stripe   │   │  Firebase  │
    │ Video  │   │  Payments  │   │  Auth/Push │
    │ Voice  │   │            │   │            │
    └────────┘   └────────────┘   └────────────┘

Technology Stack

| Layer | Technology | Purpose | |-------|-----------|---------| | Backend | Node.js, Express, TypeScript, MongoDB (Mongoose), Socket.IO | REST API, WebSocket server, business logic | | Mobile | React Native, Expo Router, TypeScript | iOS/Android native app | | Web | React, Vite, TypeScript | Browser-based web application | | Admin | React, Vite, TypeScript | Internal administration dashboard | | SDK | TypeScript, Axios, Socket.IO Client | Unified API communication layer | | Auth | Firebase Authentication + JWT | Identity verification, token management | | Payments | Stripe | Subscriptions, premium features | | Calling | Twilio Video + Twilio Voice | Real-time video/voice calls | | Storage | AWS S3 + CloudFront CDN | Media file hosting and delivery | | Email | SendGrid | Transactional emails | | SMS | Twilio Verify + Messaging | Phone verification, notifications | | Images | Cloudinary | Image optimization, transformation | | Maps | Google Maps + Places API | Location-based features |

Data Flow

A typical request lifecycle through the SDK:

User Action (e.g., tap "Like")
    │
    ▼
React Component calls: services.match.swipe({ targetUserId, action: 'like' })
    │
    ▼
MatchService.swipe() → formats request body
    │
    ▼
AxiosInstanceManager.getInstance() → retrieves configured Axios instance
    │
    ▼
AuthInterceptor (Request) → injects Bearer token from memory
    │
    ▼
HTTP POST → https://api.clinchdate.com/api/matches/swipe
    │
    ▼
Backend processes → validates, creates match record, checks for mutual like
    │
    ▼
HTTP Response → { success: true, data: { isMatch: true, matchId: '...' } }
    │
    ▼
AuthInterceptor (Response) → checks for 401, auto-refreshes if needed
    │
    ▼
MatchService → unwraps ApiResponse<T>, returns typed MatchResult
    │
    ▼
React Component receives typed data → updates UI

3. What Is the API Client SDK?

The @clinchdate/api-client package is a standalone npm library published to a private registry (or npm) that encapsulates:

  1. HTTP Client Configuration — A centrally configured Axios instance with base URLs, timeouts, default headers, and interceptors.
  2. Authentication Management — Automatic injection of JWT tokens into every request, transparent token refresh on 401 responses, and session cleanup on auth failure.
  3. Service Classes — Domain-specific classes (AuthService, UserService, MatchService, ChatService, NotificationService, PaymentService) that map 1:1 to backend API domains, each exposing typed methods.
  4. React Hooks — Pre-built hooks (useAuth, useMatch, useChat) that wrap service calls with React state management (loading, error, data).
  5. TypeScript Type Definitions — Complete request/response type contracts that ensure compile-time safety across all consumers.
  6. Error Normalization — A unified ApiClientError class that standardizes all error shapes (network failures, validation errors, server errors, auth errors) into a single predictable structure.
  7. Socket.IO Integration — Pre-configured real-time connection management for chat and notifications.

What It Is NOT

  • It is not a state management library. It does not replace Redux, Zustand, or React Context for global state.
  • It is not a UI component library. It provides data, not views.
  • It is not coupled to any specific frontend framework. While React hooks are included, the core services work in any JavaScript/TypeScript environment.

4. Core Design Principles

4.1 Single Source of Truth

Every API endpoint is defined exactly once in the SDK. If the backend changes an endpoint from /users/profile to /users/me, the change happens in one file (UserService.ts), the package is republished, and all frontends update by bumping the version.

4.2 Type Safety at the Boundary

The SDK defines TypeScript interfaces for every request and response. This creates an enforceable contract: if the backend adds a required field to UpdateProfileRequest, every frontend that fails to provide it will fail to compile. This catches integration bugs at build time, not in production.

4.3 Zero Boilerplate for Consumers

A frontend developer should never need to:

  • Construct a URL
  • Set an Authorization header
  • Handle token refresh
  • Parse error response shapes
  • Configure Axios interceptors

All of this is the SDK's responsibility.

4.4 Fail Fast, Fail Clearly

The error handling system normalizes all failures into ApiClientError with a predictable code enum. Frontend developers switch on error.code rather than inspecting HTTP status codes, error message strings, or nested response objects.

4.5 Platform Agnostic Core

The core service layer (Axios + services + types) works in Node.js, React Native, and any browser environment. React hooks are an optional convenience layer — the same SDK powers a React Native app and a potential future CLI tool.


5. Package Structure & File Breakdown

@clinchdate/api-client/
├── src/
│   ├── index.ts                    # Public API — barrel export
│   ├── config/
│   │   └── apiConfig.ts            # Environment-aware configuration
│   ├── core/
│   │   ├── AxiosInstanceManager.ts # Singleton Axios factory
│   │   └── AuthInterceptor.ts      # Token injection & refresh
│   ├── services/
│   │   ├── AuthService.ts          # Authentication operations
│   │   ├── UserService.ts          # User profile & discovery
│   │   ├── MatchService.ts         # Swiping & match management
│   │   ├── ChatService.ts          # Messaging operations
│   │   ├── NotificationService.ts  # Notification management
│   │   └── PaymentService.ts       # Subscriptions & billing
│   ├── hooks/
│   │   ├── useAuth.ts              # React auth state hook
│   │   ├── useMatch.ts             # React match state hook
│   │   └── useChat.ts              # React chat state hook
│   ├── types/
│   │   ├── common.ts               # Shared types (ApiResponse, etc.)
│   │   ├── auth.types.ts           # Auth request/response types
│   │   ├── user.types.ts           # User domain types
│   │   ├── match.types.ts          # Match domain types
│   │   ├── chat.types.ts           # Chat & message types
│   │   ├── notification.types.ts   # Notification types
│   │   └── payment.types.ts        # Payment & subscription types
│   └── errors/
│       ├── ApiClientError.ts       # Custom error class
│       └── ApiErrorCode.ts         # Error code enum
├── package.json
├── tsconfig.json
├── tsconfig.build.json
├── rollup.config.js                # Bundle configuration
├── jest.config.js                  # Test configuration
├── CHANGELOG.md
├── README.md
└── API_CLIENT_USAGE_GUIDE.md

Detailed File Purposes

src/index.ts — Public API Barrel Export

This is the only file consumers interact with. It re-exports everything the SDK exposes publicly:

// Initialization
export { initializeApiClient } from './config/apiConfig';

// Services (singleton instances)
export { services } from './services';

// Individual service classes (for testing/extension)
export { AuthService } from './services/AuthService';
export { UserService } from './services/UserService';
export { MatchService } from './services/MatchService';
export { ChatService } from './services/ChatService';
export { NotificationService } from './services/NotificationService';
export { PaymentService } from './services/PaymentService';

// React Hooks
export { useAuth } from './hooks/useAuth';
export { useMatch } from './hooks/useMatch';
export { useChat } from './hooks/useChat';

// Types (all re-exported for consumer use)
export * from './types/common';
export * from './types/auth.types';
export * from './types/user.types';
export * from './types/match.types';
export * from './types/chat.types';
export * from './types/notification.types';
export * from './types/payment.types';

// Error handling
export { ApiClientError } from './errors/ApiClientError';
export { ApiErrorCode } from './errors/ApiErrorCode';

// Core (for advanced usage)
export { AxiosInstanceManager } from './core/AxiosInstanceManager';
export { AuthInterceptor } from './core/AuthInterceptor';

Design rationale: By funneling everything through index.ts, tree-shaking is possible. If a consumer only imports services.auth, bundlers can potentially eliminate unused service code. More importantly, it provides a stable public API — internal file restructuring doesn't break imports.

src/config/apiConfig.ts — Environment Configuration

This file owns the initializeApiClient() function, which must be called once at application startup. It accepts a configuration object and uses it to construct the Axios instance and (optionally) the Socket.IO connection.

Logic implementation:

interface ApiClientConfig {
  environment: 'development' | 'staging' | 'production';
  baseURL: string;
  socketURL?: string;
  timeout?: number;        // Default: 30000ms
  enableLogging?: boolean; // Default: true in development
}

When called, this function:

  1. Validates the configuration (throws if baseURL is missing).
  2. Passes the config to AxiosInstanceManager.initialize() to create the Axios singleton.
  3. Registers AuthInterceptor request/response interceptors on the Axios instance.
  4. If socketURL is provided, initializes the Socket.IO client connection.
  5. Stores the environment setting for conditional logic elsewhere (e.g., logging in dev only).

Why a separate config file? Initialization is a one-time operation that crosses concerns (HTTP, auth, sockets). Isolating it from the core modules prevents circular dependencies and makes the initialization sequence explicit.

src/core/AxiosInstanceManager.ts — Singleton HTTP Factory

This class implements the Singleton pattern to ensure exactly one Axios instance exists throughout the application lifecycle.

Technical implementation:

class AxiosInstanceManager {
  private static instance: AxiosInstance | null = null;

  static initialize(config: { baseURL: string; timeout?: number }): void {
    this.instance = axios.create({
      baseURL: config.baseURL,
      timeout: config.timeout || 30000,
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    });
  }

  static getInstance(): AxiosInstance {
    if (!this.instance) {
      throw new Error(
        'API Client not initialized. Call initializeApiClient() first.'
      );
    }
    return this.instance;
  }
}

Why a singleton? Multiple Axios instances would mean multiple interceptor chains, multiple base URLs, and inconsistent behavior. The singleton guarantees that auth token injection, logging, and error handling apply uniformly to every request from every service.

Why throw on uninitialized access? A missing initializeApiClient() call is a developer error that should fail loudly and immediately, not silently produce undefined URLs or missing auth headers.

src/core/AuthInterceptor.ts — Token Management & Auto-Refresh

This is arguably the most critical file in the SDK. It handles:

  1. Request interception: Before every outgoing request, it reads the current access token from in-memory storage and attaches it as Authorization: Bearer <token>.
  2. Response interception (401 handling): When a response returns 401 (Unauthorized), it intercepts the failure, attempts to refresh the token using the stored refresh token, and retries the original request transparently.
  3. Refresh token rotation: When a refresh succeeds, both the new access token and new refresh token are stored.
  4. Concurrent request queuing: If multiple requests fail with 401 simultaneously, only one refresh request is made. All other failed requests are queued and retried after the refresh completes.
  5. Logout on refresh failure: If the refresh itself fails (e.g., refresh token expired), all queued requests are rejected and the user is logged out.

Technical flow:

Request made → AuthInterceptor.request → adds Bearer token
    │
    ▼
Response 200 → pass through
    │
Response 401 → is refresh already in progress?
    │            ├── YES → queue this request, wait for refresh
    │            └── NO  → start refresh
    │                        │
    │                  POST /auth/refresh { refreshToken }
    │                        │
    │                   ┌────▼────┐
    │                   │ Success │ → store new tokens → retry all queued
    │                   │ Failure │ → clear tokens → reject all queued → logout
    │                   └─────────┘

Static methods exposed:

AuthInterceptor.setTokens(accessToken: string, refreshToken: string): void
AuthInterceptor.getAccessToken(): string | null
AuthInterceptor.clearTokens(): void

Why in-memory storage? Tokens are stored in JavaScript variables, not localStorage or AsyncStorage. This is intentional:

  • In React Native, AsyncStorage is asynchronous, which complicates synchronous interceptor logic.
  • The SDK is storage-agnostic. The consuming app decides where to persist tokens (Keychain, SecureStore, encrypted storage) and calls AuthInterceptor.setTokens() on app launch.
  • In-memory storage is inherently XSS-resistant (tokens aren't accessible via document.cookie or localStorage).

src/services/ — Domain Service Classes

Each service class follows an identical pattern:

  1. Obtains the Axios instance from AxiosInstanceManager.getInstance().
  2. Defines methods that map to specific backend endpoints.
  3. Each method constructs the request (URL, method, body, query params).
  4. Each method unwraps the ApiResponse<T> envelope and returns the typed T data.
  5. Errors are caught and re-thrown as ApiClientError.

Detailed breakdown of each service:


AuthService.ts — Authentication Operations

Handles the complete authentication lifecycle: registration, login, logout, email verification, password reset, and token refresh.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | login(data) | POST | /auth/login | LoginRequest | AuthResponse | Authenticate with email/password. Returns user object + JWT tokens. Automatically calls AuthInterceptor.setTokens(). | | register(data) | POST | /auth/register | RegisterRequest | AuthResponse | Create new account. Returns user + tokens. Triggers welcome email via SendGrid. | | logout() | POST | /auth/logout | — | void | Invalidates refresh token server-side. Calls AuthInterceptor.clearTokens(). | | refreshToken(data) | POST | /auth/refresh | { refreshToken } | AuthResponse | Exchange refresh token for new token pair. Called automatically by AuthInterceptor on 401. | | verifyEmail(token) | POST | /auth/verify-email | { token } | { verified: boolean } | Confirm email address using token from verification email. | | requestPasswordReset(email) | POST | /auth/forgot-password | { email } | { message: string } | Sends password reset email. | | resetPassword(token, newPassword) | POST | /auth/reset-password | { token, newPassword } | { message: string } | Sets new password using reset token. |

Implementation logic for login():

async login(data: LoginRequest): Promise<AuthResponse> {
  const axios = AxiosInstanceManager.getInstance();
  const response = await axios.post<ApiResponse<AuthResponse>>(
    '/auth/login',
    data
  );
  const authData = response.data.data;
  
  // Automatically store tokens for subsequent requests
  AuthInterceptor.setTokens(
    authData.tokens.accessToken,
    authData.tokens.refreshToken
  );
  
  return authData;
}

Critical detail: The login() and register() methods have a side effect — they call AuthInterceptor.setTokens(). This means the moment login succeeds, every subsequent request from any service automatically includes the auth header. No additional wiring required by the consumer.


UserService.ts — User Profile & Discovery

Manages user profiles, avatars, account deletion, user discovery (the pool of potential matches), and user blocking.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | getProfile(userId?) | GET | /users/profile or /users/:id | — | User | Fetch own profile (no param) or another user's profile. | | updateProfile(data) | PATCH | /users/profile | UpdateProfileRequest | User | Update profile fields (name, bio, preferences, location, etc.). | | uploadAvatar(file) | POST | /users/avatar | FormData | { url: string } | Upload profile image. Backend processes via Cloudinary. | | deleteAccount() | DELETE | /users/account | — | void | Permanent account deletion. Triggers data cleanup pipeline. | | getDiscoverUsers(limit) | GET | /users/discover?limit=N | — | User[] | Fetch potential matches based on preferences, location, and exclusion filters. | | blockUser(userId) | POST | /users/block/:id | — | void | Block a user. Removes from discovery and match pools. | | getBlockedUsers() | GET | /users/blocked | — | User[] | List all blocked users. |

getDiscoverUsers() technical detail: This is the core of the dating experience. The backend applies multiple filters: gender preferences, age range, geographic radius (using MongoDB geospatial queries), exclusion of already-swiped users, exclusion of blocked users, and randomization. The SDK simply passes the limit parameter; all filtering logic lives server-side.

uploadAvatar() implementation note: This is the only service method that sends multipart/form-data instead of JSON. The method constructs a FormData object and overrides the Content-Type header:

async uploadAvatar(file: File | Blob): Promise<{ url: string }> {
  const formData = new FormData();
  formData.append('avatar', file);
  
  const axios = AxiosInstanceManager.getInstance();
  const response = await axios.post<ApiResponse<{ url: string }>>(
    '/users/avatar',
    formData,
    { headers: { 'Content-Type': 'multipart/form-data' } }
  );
  
  return response.data.data;
}

MatchService.ts — Swiping & Match Management

Handles the core dating mechanics: swiping, match retrieval, unmatching, and user reporting.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | swipe(data) | POST | /matches/swipe | SwipeRequest | SwipeResponse | Record a like/pass. Returns isMatch: true if mutual. | | getMatches() | GET | /matches | — | Match[] | All matches (active + expired). | | getActiveMatches() | GET | /matches/active | — | Match[] | Only active (non-unmatched) matches. | | getMatch(matchId) | GET | /matches/:id | — | Match | Single match details including both user profiles. | | unmatch(matchId) | DELETE | /matches/:id | — | void | Dissolve a match. Removes chat history access. | | reportUser(userId, reason) | POST | /matches/report | { userId, reason } | void | Report inappropriate behavior. Triggers admin review. |

swipe() implementation logic:

The SwipeRequest type defines:

interface SwipeRequest {
  targetUserId: string;
  action: 'like' | 'pass';
}

interface SwipeResponse {
  isMatch: boolean;
  matchId?: string;    // Present only if isMatch is true
  match?: Match;       // Full match object if mutual
}

When action is 'like', the backend checks if the target user has already liked the current user. If so, it creates a Match document, sends push notifications to both users via Firebase, and returns isMatch: true. The SDK returns this typed response so the frontend can immediately show the "It's a Match!" screen.


ChatService.ts — Messaging Operations

Manages the messaging system between matched users, including message CRUD, conversation listing, read receipts, and search.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | sendMessage(data) | POST | /chat/messages | SendMessageRequest | Message | Send a text message within a match. | | getMessages(matchId, limit?) | GET | /chat/:matchId/messages | — | Message[] | Fetch message history for a match (paginated). | | getConversations() | GET | /chat/conversations | — | Conversation[] | List all active conversations with last message preview. | | markAsRead(messageId) | PATCH | /chat/messages/:id/read | — | void | Mark a specific message as read. | | deleteMessage(messageId) | DELETE | /chat/messages/:id | — | void | Soft-delete a message (still visible to other party). | | searchMessages(matchId, query) | GET | /chat/:matchId/search?q=query | — | Message[] | Full-text search within a conversation. |

Real-time integration: While ChatService handles the REST API for persistence, real-time message delivery happens through Socket.IO. The SDK's socket layer emits message:new events and listens for incoming messages. The useChat hook bridges both: it calls getMessages() for initial load and subscribes to socket events for live updates.


NotificationService.ts — Notification Management

Handles in-app notification retrieval, read status, deletion, and notification preference configuration.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | getNotifications(limit?) | GET | /notifications | — | Notification[] | Fetch notifications list. | | getUnreadCount() | GET | /notifications/unread-count | — | { count: number } | Badge count for UI indicators. | | markAsRead(id) | PATCH | /notifications/:id/read | — | void | Mark single notification as read. | | deleteNotification(id) | DELETE | /notifications/:id | — | void | Remove a notification. | | getPreferences() | GET | /notifications/preferences | — | NotificationPreferences | Current push/email/SMS settings. | | updatePreferences(data) | PATCH | /notifications/preferences | NotificationPreferences | NotificationPreferences | Update notification channel settings. |


PaymentService.ts — Subscriptions & Billing

Integrates with Stripe for premium subscription management, payment method handling, and plan selection.

| Method | HTTP | Endpoint | Request Type | Response Type | Description | |--------|------|----------|-------------|--------------|-------------| | getSubscription() | GET | /payments/subscription | — | Subscription | Current subscription details and status. | | createSubscription(data) | POST | /payments/subscription | CreateSubscriptionRequest | Subscription | Subscribe to a plan via Stripe. | | cancelSubscription() | DELETE | /payments/subscription | — | void | Cancel active subscription (effective at period end). | | getPaymentMethods() | GET | /payments/methods | — | PaymentMethod[] | List saved cards/payment methods. | | addPaymentMethod(token) | POST | /payments/methods | { token } | PaymentMethod | Add a new payment method via Stripe token. | | getSubscriptionPlans() | GET | /payments/plans | — | Plan[] | Available subscription tiers with pricing. |


6. Initialization & Configuration System

The SDK requires a single initialization call before any service can be used. This is by design — it prevents services from executing requests against an unconfigured Axios instance.

Configuration Interface

interface ApiClientConfig {
  environment: 'development' | 'staging' | 'production';
  baseURL: string;           // Required. Backend API base URL.
  socketURL?: string;        // Optional. Socket.IO server URL.
  timeout?: number;          // Optional. Request timeout in ms. Default: 30000.
  enableLogging?: boolean;   // Optional. Console logging. Default: true in dev.
}

Initialization per Platform

Web App (Next.js / React):

// src/pages/_app.tsx or src/main.tsx
import { useEffect } from 'react';
import { initializeApiClient } from '@clinchdate/api-client';

function App({ Component, pageProps }) {
  useEffect(() => {
    initializeApiClient({
      environment: process.env.NEXT_PUBLIC_API_ENV || 'development',
      baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api',
      socketURL: process.env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:5000',
    });
  }, []);

  return <Component {...pageProps} />;
}

Mobile App (React Native / Expo):

// App.tsx
import { useEffect } from 'react';
import { initializeApiClient } from '@clinchdate/api-client';

export default function App() {
  useEffect(() => {
    initializeApiClient({
      environment: __DEV__ ? 'development' : 'production',
      baseURL: __DEV__
        ? 'http://10.0.2.2:5000/api'  // Android emulator localhost alias
        : 'https://api.clinchdate.com/api',
      socketURL: __DEV__
        ? 'http://10.0.2.2:5000'
        : 'https://api.clinchdate.com',
    });
  }, []);

  return <Navigation />;
}

Note on React Native localhost: Android emulators cannot resolve localhost to the host machine. 10.0.2.2 is the standard alias. iOS simulators can use localhost directly. The SDK itself is platform-agnostic; this is a network configuration concern.

Admin Panel (React / Vite):

// src/main.tsx
import { initializeApiClient } from '@clinchdate/api-client';

initializeApiClient({
  environment: import.meta.env.MODE as any,
  baseURL: import.meta.env.VITE_API_URL,
});

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode><App /></React.StrictMode>
);

7. HTTP Layer — Axios Instance Manager

Why Axios?

Axios was chosen over fetch for several reasons specific to ClinchDate's requirements:

  1. Interceptors: Native request/response interceptors are essential for the auth token injection pattern. fetch requires manual wrapper functions.
  2. Automatic JSON parsing: Axios automatically parses JSON responses. fetch requires manual .json() calls.
  3. Request cancellation: Axios supports CancelToken for aborting in-flight requests (useful when navigating away from screens).
  4. Timeout support: Built-in timeout configuration. fetch requires AbortController + setTimeout boilerplate.
  5. React Native compatibility: Axios works identically in Node.js, browsers, and React Native. fetch polyfills vary across React Native versions.

Singleton Pattern Implementation

class AxiosInstanceManager {
  private static instance: AxiosInstance | null = null;

  static initialize(config: AxiosConfig): void {
    if (this.instance) {
      console.warn('API Client already initialized. Reinitializing...');
    }

    this.instance = axios.create({
      baseURL: config.baseURL,
      timeout: config.timeout || 30000,
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-Client-Version': SDK_VERSION,    // Tracks SDK version on server
        'X-Platform': detectPlatform(),      // 'web' | 'ios' | 'android'
      },
    });
  }

  static getInstance(): AxiosInstance {
    if (!this.instance) {
      throw new Error(
        '@clinchdate/api-client: Not initialized. ' +
        'Call initializeApiClient() in your app entry point.'
      );
    }
    return this.instance;
  }

  static destroy(): void {
    this.instance = null;
  }
}

Custom Headers

The SDK injects two custom headers on every request:

  • X-Client-Version: The SDK version (e.g., 1.2.3). Allows the backend to detect outdated clients and return upgrade-required responses.
  • X-Platform: The platform identifier (web, ios, android, admin). Allows the backend to apply platform-specific logic (e.g., different push notification payloads).

8. Authentication Interceptor System

Request Interceptor

Runs before every outgoing request:

axiosInstance.interceptors.request.use(
  (config) => {
    const token = AuthInterceptor.getAccessToken();
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

Response Interceptor — 401 Auto-Refresh

This is the most complex piece of the SDK. The implementation handles the race condition where multiple concurrent requests all receive 401:

let isRefreshing = false;
let failedQueue: Array<{
  resolve: (token: string) => void;
  reject: (error: any) => void;
}> = [];

const processQueue = (error: any, token: string | null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token!);
    }
  });
  failedQueue = [];
};

axiosInstance.interceptors.response.use(
  (response) => response,   // Pass through successful responses
  async (error) => {
    const originalRequest = error.config;

    // Only handle 401 and only retry once
    if (error.response?.status !== 401 || originalRequest._retry) {
      return Promise.reject(error);
    }

    if (isRefreshing) {
      // Another refresh is in progress — queue this request
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then((token) => {
        originalRequest.headers.Authorization = `Bearer ${token}`;
        return axiosInstance(originalRequest);
      });
    }

    originalRequest._retry = true;
    isRefreshing = true;

    try {
      const refreshToken = AuthInterceptor.getRefreshToken();
      const response = await axios.post(
        `${baseURL}/auth/refresh`,
        { refreshToken }
      );

      const { accessToken, refreshToken: newRefreshToken } = response.data.data.tokens;
      AuthInterceptor.setTokens(accessToken, newRefreshToken);

      processQueue(null, accessToken);

      originalRequest.headers.Authorization = `Bearer ${accessToken}`;
      return axiosInstance(originalRequest);
    } catch (refreshError) {
      processQueue(refreshError, null);
      AuthInterceptor.clearTokens();
      // Emit logout event for the consuming app to handle
      return Promise.reject(refreshError);
    } finally {
      isRefreshing = false;
    }
  }
);

Why this matters: Without concurrent request queuing, if a user's feed loads 5 images simultaneously and the token expires, the app would make 5 refresh token requests. The server would likely invalidate the first 4 refresh tokens (refresh token rotation), causing cascading auth failures. The queue ensures exactly one refresh occurs.


9. Service Layer — Detailed Breakdown

All services follow this pattern:

class SomeService {
  private get axios() {
    return AxiosInstanceManager.getInstance();
  }

  async someMethod(data: RequestType): Promise<ResponseType> {
    try {
      const response = await this.axios.post<ApiResponse<ResponseType>>(
        '/endpoint',
        data
      );
      return response.data.data;  // Unwrap the ApiResponse envelope
    } catch (error) {
      throw ApiClientError.fromAxiosError(error);
    }
  }
}

The ApiResponse<T> envelope: The backend wraps all responses in a standard envelope:

interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  meta?: {
    page?: number;
    limit?: number;
    total?: number;
  };
}

Every service method strips this envelope and returns just T. Consumers never interact with success or meta directly (though paginated endpoints may expose meta through extended return types).


10. React Hooks Layer

The hooks provide React-idiomatic wrappers around services with built-in state management.

useAuth Hook

function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<ApiClientError | null>(null);

  const login = async (data: LoginRequest) => {
    setLoading(true);
    setError(null);
    try {
      const response = await services.auth.login(data);
      setUser(response.user);
      return response;
    } catch (err) {
      setError(err as ApiClientError);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  const register = async (data: RegisterRequest) => { /* similar pattern */ };
  const logout = async () => { /* similar pattern */ };

  return { user, loading, error, login, register, logout };
}

Pattern: Every hook exposes { data, loading, error, ...methods }. This is deliberately similar to popular data-fetching libraries (React Query, SWR) so the mental model is familiar to React developers.

useMatch Hook

function useMatch() {
  const [matches, setMatches] = useState<Match[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<ApiClientError | null>(null);

  const getMatches = async () => { /* fetches and sets matches */ };
  const swipe = async (data: SwipeRequest) => { /* calls service, returns result */ };
  const unmatch = async (matchId: string) => { /* calls service, removes from state */ };

  return { matches, loading, error, getMatches, swipe, unmatch };
}

useChat Hook

function useChat() {
  const [conversations, setConversations] = useState<Conversation[]>([]);
  const [messages, setMessages] = useState<Message[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<ApiClientError | null>(null);

  const getConversations = async () => { /* fetches conversation list */ };
  const getMessages = async (matchId: string) => { /* fetches message history */ };
  const sendMessage = async (matchId: string, text: string, recipientId: string) => {
    /* sends message, appends to local state optimistically */
  };

  return { conversations, messages, loading, error, getConversations, getMessages, sendMessage };
}

Optimistic updates in useChat: The sendMessage method appends the new message to the local messages array before the server confirms. If the server returns an error, the message is removed and the error state is set. This provides instant UI feedback while the network request completes.


11. Type System & Contracts

The SDK exports comprehensive TypeScript types that enforce compile-time contracts between frontend and backend.

Core Types

// Generic API response wrapper
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  meta?: PaginationMeta;
}

interface PaginationMeta {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
}

interface PaginatedResponse<T> {
  items: T[];
  meta: PaginationMeta;
}

Auth Types

interface LoginRequest {
  email: string;
  password: string;
}

interface RegisterRequest {
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;      // ISO date string
  gender: 'male' | 'female' | 'non-binary' | 'other';
  interestedIn: ('male' | 'female' | 'non-binary' | 'other')[];
}

interface AuthResponse {
  user: User;
  tokens: {
    accessToken: string;
    refreshToken: string;
    expiresIn: number;       // Seconds until access token expires
  };
}

User Types

interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  interestedIn: string[];
  bio?: string;
  avatar?: string;
  photos?: string[];
  location?: Location;
  preferences?: UserPreferences;
  verified: boolean;
  premium: boolean;
  createdAt: string;
  updatedAt: string;
}

interface Location {
  type: 'Point';
  coordinates: [number, number];  // [longitude, latitude]
  city?: string;
  country?: string;
}

interface UpdateProfileRequest {
  firstName?: string;
  lastName?: string;
  bio?: string;
  photos?: string[];
  location?: Location;
  preferences?: UserPreferences;
}

interface UserPreferences {
  ageRange: { min: number; max: number };
  maxDistance: number;           // Kilometers
  showMe: ('male' | 'female' | 'non-binary' | 'other')[];
}

Match Types

interface Match {
  id: string;
  users: [string, string];     // User IDs
  profiles: [MatchUser, MatchUser];
  status: 'active' | 'unmatched' | 'expired';
  createdAt: string;
  lastMessage?: Message;
}

interface MatchUser {
  id: string;
  firstName: string;
  avatar?: string;
  bio?: string;
}

interface SwipeRequest {
  targetUserId: string;
  action: 'like' | 'pass';
}

interface SwipeResponse {
  isMatch: boolean;
  matchId?: string;
  match?: Match;
}

Chat Types

interface Message {
  id: string;
  matchId: string;
  senderId: string;
  recipientId: string;
  text: string;
  type: 'text' | 'image' | 'gif' | 'system';
  read: boolean;
  createdAt: string;
  updatedAt: string;
}

interface Conversation {
  matchId: string;
  participant: MatchUser;
  lastMessage: Message;
  unreadCount: number;
}

interface SendMessageRequest {
  text: string;
  matchId: string;
  recipientId: string;
  type?: 'text' | 'image' | 'gif';
}

Notification Types

interface Notification {
  id: string;
  type: 'match' | 'message' | 'like' | 'system' | 'promo';
  title: string;
  body: string;
  data?: Record<string, any>;
  read: boolean;
  createdAt: string;
}

interface NotificationPreferences {
  push: boolean;
  email: boolean;
  sms: boolean;
  matchAlerts: boolean;
  messageAlerts: boolean;
  promotions: boolean;
}

Payment Types

interface Subscription {
  id: string;
  plan: string;
  status: 'active' | 'canceled' | 'past_due' | 'trialing';
  currentPeriodEnd: string;
  cancelAtPeriodEnd: boolean;
}

interface Plan {
  id: string;
  name: string;
  price: number;
  currency: string;
  interval: 'month' | 'year';
  features: string[];
}

interface PaymentMethod {
  id: string;
  brand: string;        // 'visa', 'mastercard', etc.
  last4: string;
  expiryMonth: number;
  expiryYear: number;
  isDefault: boolean;
}

interface CreateSubscriptionRequest {
  planId: string;
  paymentMethodId?: string;
}

12. Error Handling Architecture

ApiClientError Class

class ApiClientError extends Error {
  code: ApiErrorCode;
  statusCode?: number;
  originalError?: any;
  validationErrors?: Record<string, string[]>;

  constructor(
    message: string,
    code: ApiErrorCode,
    statusCode?: number,
    originalError?: any
  ) {
    super(message);
    this.name = 'ApiClientError';
    this.code = code;
    this.statusCode = statusCode;
    this.originalError = originalError;
  }

  static fromAxiosError(error: AxiosError): ApiClientError {
    if (!error.response) {
      // Network error (no response received)
      return new ApiClientError(
        'Network error. Please check your connection.',
        ApiErrorCode.NETWORK_ERROR,
        undefined,
        error
      );
    }

    const status = error.response.status;
    const data = error.response.data as any;

    switch (status) {
      case 400:
        return new ApiClientError(
          data.message || 'Invalid request',
          ApiErrorCode.VALIDATION_ERROR,
          400,
          data
        );
      case 401:
        return new ApiClientError(
          'Authentication required',
          ApiErrorCode.UNAUTHORIZED,
          401,
          data
        );
      case 403:
        return new ApiClientError(
          'Permission denied',
          ApiErrorCode.FORBIDDEN,
          403,
          data
        );
      case 404:
        return new ApiClientError(
          data.message || 'Resource not found',
          ApiErrorCode.NOT_FOUND,
          404,
          data
        );
      case 429:
        return new ApiClientError(
          'Too many requests. Please slow down.',
          ApiErrorCode.RATE_LIMITED,
          429,
          data
        );
      default:
        return new ApiClientError(
          data.message || 'Server error',
          ApiErrorCode.SERVER_ERROR,
          status,
          data
        );
    }
  }
}

ApiErrorCode Enum

enum ApiErrorCode {
  NETWORK_ERROR = 'NETWORK_ERROR',
  UNAUTHORIZED = 'UNAUTHORIZED',
  FORBIDDEN = 'FORBIDDEN',
  NOT_FOUND = 'NOT_FOUND',
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  RATE_LIMITED = 'RATE_LIMITED',
  SERVER_ERROR = 'SERVER_ERROR',
  TIMEOUT = 'TIMEOUT',
  UNKNOWN = 'UNKNOWN',
}

Consumer Usage Pattern

import { ApiClientError, ApiErrorCode } from '@clinchdate/api-client';

try {
  await services.auth.login({ email, password });
} catch (error) {
  if (error instanceof ApiClientError) {
    switch (error.code) {
      case ApiErrorCode.UNAUTHORIZED:
        showToast('Invalid email or password');
        break;
      case ApiErrorCode.NETWORK_ERROR:
        showToast('No internet connection');
        break;
      case ApiErrorCode.VALIDATION_ERROR:
        // Display field-level errors
        if (error.validationErrors) {
          Object.entries(error.validationErrors).forEach(([field, msgs]) => {
            setFieldError(field, msgs[0]);
          });
        }
        break;
      case ApiErrorCode.RATE_LIMITED:
        showToast('Too many attempts. Please wait.');
        break;
      default:
        showToast('Something went wrong. Please try again.');
    }
  }
}

13. Socket.IO Real-Time Layer

The SDK optionally initializes a Socket.IO client for real-time features (chat messages, typing indicators, online status, notifications).

Connection Management

import { io, Socket } from 'socket.io-client';

let socket: Socket | null = null;

function initializeSocket(url: string, accessToken: string): Socket {
  socket = io(url, {
    auth: { token: accessToken },
    transports: ['websocket', 'polling'],  // Prefer WebSocket, fallback to polling
    reconnection: true,
    reconnectionAttempts: 10,
    reconnectionDelay: 1000,
  });

  socket.on('connect', () => {
    console.log('Socket connected:', socket.id);
  });

  socket.on('disconnect', (reason) => {
    console.log('Socket disconnected:', reason);
  });

  return socket;
}

Events

| Event | Direction | Payload | Description | |-------|-----------|---------|-------------| | message:new | Server → Client | Message | New incoming message | | message:send | Client → Server | SendMessageRequest | Send a message (alternative to REST) | | typing:start | Client → Server | { matchId, userId } | User started typing | | typing:stop | Client → Server | { matchId, userId } | User stopped typing | | typing:indicator | Server → Client | { matchId, userId } | Other user is typing | | user:online | Server → Client | { userId } | User came online | | user:offline | Server → Client | { userId } | User went offline | | match:new | Server → Client | Match | New match created (mutual like) | | notification:new | Server → Client | Notification | New notification |


14. Integration Guide — Web App (React/Vite)

Step 1: Install

cd web-app
npm install @clinchdate/api-client

Step 2: Initialize at Entry Point

// src/main.tsx
import { initializeApiClient } from '@clinchdate/api-client';

initializeApiClient({
  environment: import.meta.env.VITE_API_ENV || 'development',
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api',
  socketURL: import.meta.env.VITE_SOCKET_URL || 'http://localhost:5000',
});

Step 3: Use in Components

import { useAuth, services } from '@clinchdate/api-client';

function Dashboard() {
  const { user, logout } = useAuth();
  const [matches, setMatches] = useState([]);

  useEffect(() => {
    services.match.getActiveMatches().then(setMatches);
  }, []);

  return (
    <div>
      <h1>Welcome, {user?.firstName}</h1>
      <p>{matches.length} active matches</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

15. Integration Guide — Mobile App (React Native/Expo)

Step 1: Install

cd mobile-app
npm install @clinchdate/api-client

Step 2: Initialize in App Entry

// App.tsx
import { initializeApiClient, AuthInterceptor } from '@clinchdate/api-client';
import * as SecureStore from 'expo-secure-store';

export default function App() {
  useEffect(() => {
    initializeApiClient({
      environment: __DEV__ ? 'development' : 'production',
      baseURL: __DEV__
        ? 'http://10.0.2.2:5000/api'
        : 'https://api.clinchdate.com/api',
    });

    // Restore tokens from secure storage on app launch
    const restoreTokens = async () => {
      const accessToken = await SecureStore.getItemAsync('accessToken');
      const refreshToken = await SecureStore.getItemAsync('refreshToken');
      if (accessToken && refreshToken) {
        AuthInterceptor.setTokens(accessToken, refreshToken);
      }
    };
    restoreTokens();
  }, []);

  return <Navigation />;
}

Step 3: Persist Tokens After Login

import { services, AuthInterceptor } from '@clinchdate/api-client';
import * as SecureStore from 'expo-secure-store';

async function handleLogin(email: string, password: string) {
  const response = await services.auth.login({ email, password });
  
  // Persist tokens securely for app restart
  await SecureStore.setItemAsync('accessToken', response.tokens.accessToken);
  await SecureStore.setItemAsync('refreshToken', response.tokens.refreshToken);
  
  return response.user;
}

16. Integration Guide — Admin Panel

The admin panel follows the same pattern as the web app. The key difference is that admin endpoints may require elevated permissions, which are handled server-side via role-based access control (RBAC). The SDK doesn't differentiate — it sends the same auth token, and the backend validates the user's role.

// Admin-specific service calls
const users = await services.admin.getAllUsers({ page: 1, limit: 50 });
const reports = await services.admin.getReports({ status: 'pending' });
await services.admin.banUser(userId, { reason: 'Inappropriate behavior', duration: '30d' });

17. Environment Configuration

Environment Variables by Platform

Web App (.env):

VITE_API_ENV=development
VITE_API_URL=http://localhost:5000/api
VITE_SOCKET_URL=http://localhost:5000

Next.js (.env.local):

NEXT_PUBLIC_API_ENV=development
NEXT_PUBLIC_API_URL=http://localhost:5000/api
NEXT_PUBLIC_SOCKET_URL=http://localhost:5000

Staging (.env.staging):

VITE_API_ENV=staging
VITE_API_URL=https://staging-api.clinchdate.com/api
VITE_SOCKET_URL=https://staging-api.clinchdate.com

Production (.env.production):

VITE_API_ENV=production
VITE_API_URL=https://api.clinchdate.com/api
VITE_SOCKET_URL=https://api.clinchdate.com

18. Caching Strategy

The SDK does not implement caching internally (by design — caching strategies vary significantly between mobile and web). Instead, it provides patterns for consumers to implement caching.

In-Memory Cache Pattern

const cache = new Map<string, { data: any; timestamp: number }>();

async function cachedGet<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlMs: number = 60000
): Promise<T> {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < ttlMs) {
    return cached.data as T;
  }

  const data = await fetcher();
  cache.set(key, { data, timestamp: Date.now() });
  return data;
}

// Usage
const profile = await cachedGet(
  'user-profile',
  () => services.user.getProfile(),
  300000  // Cache for 5 minutes
);

React Query Integration

import { useQuery } from '@tanstack/react-query';
import { services } from '@clinchdate/api-client';

function useProfile() {
  return useQuery({
    queryKey: ['profile'],
    queryFn: () => services.user.getProfile(),
    staleTime: 5 * 60 * 1000,  // 5 minutes
  });
}

19. Testing Strategy

Unit Testing Services

import { AuthService } from '@clinchdate/api-client';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('AuthService', () => {
  let authService: AuthService;

  beforeEach(() => {
    authService = new AuthService();
  });

  it('should login and return user with tokens', async () => {
    const mockResponse = {
      data: {
        success: true,
        data: {
          user: { id: '123', email: '[email protected]', firstName: 'John' },
          tokens: { accessToken: 'at_123', refreshToken: 'rt_123', expiresIn: 3600 },
        },
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockResponse);

    const result = await authService.login({
      email: '[email protected]',
      password: 'SecureP@ss1',
    });

    expect(result.user.email).toBe('[email protected]');
    expect(result.tokens.accessToken).toBe('at_123');
    expect(mockedAxios.post).toHaveBeenCalledWith(
      '/auth/login',
      expect.objectContaining({ email: '[email protected]' })
    );
  });

  it('should throw ApiClientError on 401', async () => {
    mockedAxios.post.mockRejectedValueOnce({
      response: { status: 401, data: { message: 'Invalid credentials' } },
    });

    await expect(
      authService.login({ email: '[email protected]', password: 'wrong' })
    ).rejects.toThrow();
  });
});

Integration Testing with MSW

import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { initializeApiClient, services } from '@clinchdate/api-client';

const server = setupServer(
  rest.get('http://localhost:5000/api/users/profile', (req, res, ctx) => {
    return res(ctx.json({
      success: true,
      data: { id: '1', firstName: 'Adrian', email: '[email protected]' },
    }));
  })
);

beforeAll(() => {
  server.listen();
  initializeApiClient({
    environment: 'development',
    baseURL: 'http://localhost:5000/api',
  });
});

afterAll(() => server.close());

test('fetches user profile', async () => {
  const profile = await services.user.getProfile();
  expect(profile.firstName).toBe('Adrian');
});

20. Security Considerations

  1. Tokens in memory only. The SDK stores JWT tokens in JavaScript variables, not in localStorage, sessionStorage, or cookies. This eliminates XSS-based token theft from storage APIs. The consuming app is responsible for persisting tokens in platform-appropriate secure storage (Keychain, SecureStore, encrypted SharedPreferences).

  2. Refresh token rotation. Every successful token refresh invalidates the previous refresh token and issues a new one. If a refresh token is stolen and used, the legitimate user's next refresh will fail, triggering a forced re-authentication — effectively detecting the breach.

  3. HTTPS enforced. Production base URLs must use HTTPS. The SDK does not enforce this (to allow local HTTP development), but deployment documentation mandates TLS.

  4. No sensitive data in URLs. All sensitive data (passwords, tokens, payment info) is sent in request bodies, never as URL parameters.

  5. Rate limiting awareness. The SDK surfaces 429 Rate Limited errors as ApiErrorCode.RATE_LIMITED, allowing frontends to implement backoff UI patterns.


21. Performance Optimization

  1. Request deduplication. Multiple components requesting the same data simultaneously should use a shared cache or React Query to avoid duplicate network calls.

  2. Pagination. All list endpoints support pagination. Use limit and page parameters to load data incrementally.

  3. Image optimization. Avatar and photo URLs from the backend are Cloudinary URLs that support on-the-fly transformations. Append width/height parameters to reduce payload: avatar_url + '?w=200&h=200&fit=crop'.

  4. Socket.IO transport. The SDK prefers WebSocket transport over long-polling. Ensure your deployment infrastructure supports WebSocket connections (some load balancers require explicit configuration).

  5. Bundle size. The SDK is tree-shakeable. Only import what you need:

// Good — tree-shakeable
import { services } from '@clinchdate/api-client';

// Also good — even more specific
import { AuthService } from '@clinchdate/api-client';

22. Troubleshooting & Common Issues

"API Client not initialized"

Cause: A service method was called before initializeApiClient(). Fix: Ensure initializeApiClient() is called in your app's entry point (e.g., _app.tsx, App.tsx, main.tsx) before any component renders.

"Network Error" on Android Emulator

Cause: Android emulators cannot resolve localhost. Fix: Use http://10.0.2.2:5000/api instead of http://localhost:5000/api.

401 Loops (Infinite Token Refresh)

Cause: Refresh token endpoint itself returns 401 (e.g., refresh token expired or blacklisted). Fix: The SDK handles this by checking originalRequest._retry to prevent infinite loops. If you see this in production, the user's session has expired and they need to re-authenticate.

TypeScript Compilation Errors After SDK Update

Cause: The SDK added required fields to a request type or changed a response shape. Fix: Check the CHANGELOG.md for breaking changes. Update your code to match the new type contracts.

CORS Errors in Web App

Cause: Backend CORS configuration doesn't include your frontend's origin. Fix: This is a backend configuration issue, not an SDK issue. Ensure the backend's CORS middleware includes your frontend URL.


23. API Routes Reference

Authentication Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | POST | /api/auth/register | Create new account | No | | POST | /api/auth/login | Authenticate user | No | | POST | /api/auth/logout | Invalidate session | Yes | | POST | /api/auth/refresh | Refresh JWT tokens | No* | | POST | /api/auth/verify-email | Confirm email address | No | | POST | /api/auth/forgot-password | Request password reset | No | | POST | /api/auth/reset-password | Set new password | No |

*Requires valid refresh token in request body.

User Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | GET | /api/users/profile | Get own profile | Yes | | GET | /api/users/:id | Get user profile by ID | Yes | | PATCH | /api/users/profile | Update own profile | Yes | | POST | /api/users/avatar | Upload profile avatar | Yes | | DELETE | /api/users/account | Delete account permanently | Yes | | GET | /api/users/discover | Get discoverable users | Yes | | POST | /api/users/block/:id | Block a user | Yes | | GET | /api/users/blocked | List blocked users | Yes |

Match Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | POST | /api/matches/swipe | Like or pass on user | Yes | | GET | /api/matches | Get all matches | Yes | | GET | /api/matches/active | Get active matches | Yes | | GET | /api/matches/:id | Get single match | Yes | | DELETE | /api/matches/:id | Unmatch | Yes | | POST | /api/matches/report | Report a user | Yes |

Chat Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | POST | /api/chat/messages | Send message | Yes | | GET | /api/chat/:matchId/messages | Get messages for match | Yes | | GET | /api/chat/conversations | List all conversations | Yes | | PATCH | /api/chat/messages/:id/read | Mark message as read | Yes | | DELETE | /api/chat/messages/:id | Delete message | Yes | | GET | /api/chat/:matchId/search | Search messages | Yes |

Notification Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | GET | /api/notifications | Get notifications | Yes | | GET | /api/notifications/unread-count | Get unread count | Yes | | PATCH | /api/notifications/:id/read | Mark as read | Yes | | DELETE | /api/notifications/:id | Delete notification | Yes | | GET | /api/notifications/preferences | Get preferences | Yes | | PATCH | /api/notifications/preferences | Update preferences | Yes |

Payment Routes

| Method | Route | Description | Auth Required | |--------|-------|-------------|:---:| | GET | /api/payments/subscription | Get current subscription | Yes | | POST | /api/payments/subscription | Create subscription | Yes | | DELETE | /api/payments/subscription | Cancel subscription | Yes | | GET | /api/payments/methods | List payment methods | Yes | | POST | /api/payments/methods | Add payment method | Yes | | GET | /api/payments/plans | Get available plans | Yes |


24. Deployment & Publishing

Building the SDK

cd api-client
npm run build       # Compiles TypeScript → JavaScript + declarations
npm run lint        # ESLint check
npm run test        # Run test suite

Publishing

# Bump version
npm version patch   # or minor / major

# Publish to registry
npm publish --access restricted   # Private registry
# or
npm publish                        # Public npm

Consuming a New Version

# In any frontend project
npm update @clinchdate/api-client
# or specify exact version
npm install @clinchdate/[email protected]

CI/CD Integration

The SDK should be published automatically on tagged releases:

# .github/workflows/publish.yml
name: Publish SDK
on:
  push:
    tags: ['v*']
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm run build
      - run: npm test
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

25. Changelog & Versioning

The SDK follows Semantic Versioning (SemVer):

  • MAJOR (x.0.0): Breaking changes to public API (renamed methods, removed endpoints, changed type shapes).
  • MINOR (0.x.0): New features, new service methods, new types (backward compatible).
  • PATCH (0.0.x): Bug fixes, interna