Skip to main content

Overview

Cat Web implements a clean API service architecture that handles JWT token authentication, API requests, and error handling. The ApiService class provides static methods for API calls, while the useApiService hook integrates with Auth0 for token management.

API Service Architecture

The ApiService class provides a centralized way to make authenticated API requests:
src/services/api-service.ts
export class ApiService {
    static accessTokenProvider: () => Promise<string> = async () => {
        throw new Error('Access token provider not set');
    };
    
    static setAccessToken(accessTokenProvider: () => Promise<string>) {
        this.accessTokenProvider = accessTokenProvider;
    }

    static async getPhoneNumbers(getAccessToken: () => Promise<string> = this.accessTokenProvider) {
        const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/phone-numbers`, {
            headers: {
                Authorization: `Bearer ${await getAccessToken?.()}`,
            },
        });
        return await res.json();
    }

    static async getContainers(getAccessToken: () => Promise<string> = this.accessTokenProvider) {
        const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/containers`, {
            headers: {
                Authorization: `Bearer ${await getAccessToken?.()}`,
            },
        });
        return await res.json();
    }
}

Key Design Principles

Static Methods

All API methods are static, allowing easy access without instantiation

Token Provider Pattern

Delegates token retrieval to Auth0’s getAccessTokenSilently function

Environment Variables

API base URL is configured via VITE_API_BASE_URL environment variable

Bearer Authentication

JWT tokens are included in the Authorization header for all requests

useApiService Hook

The useApiService hook bridges Auth0 authentication with the API service:
src/hooks/useApiService.ts
import { ApiService } from "../services/api-service"
import { useAuth0 } from '@auth0/auth0-react'

const useApiService = () => {
    const { getAccessTokenSilently } = useAuth0();
    ApiService.setAccessToken(getAccessTokenSilently);
    
    return ApiService;
}

export default useApiService;

How It Works

1

Import the Hook

Import useApiService in your React component:
import useApiService from '@/hooks/useApiService';
2

Initialize in Component

Call the hook to get the configured API service:
const ApiService = useApiService();
3

Make API Calls

Use the service methods with automatic token injection:
const phoneNumbers = await ApiService.getPhoneNumbers();
const containers = await ApiService.getContainers();
The hook automatically configures the API service with Auth0’s token provider each time it’s called, ensuring fresh tokens are always used.

JWT Token Management

Automatic Token Retrieval

Auth0’s getAccessTokenSilently function handles token management:
If a valid token exists in memory, it’s returned immediately:
const token = await getAccessTokenSilently();
// Returns cached token if not expired

Token in API Requests

Tokens are included in the Authorization header using the Bearer scheme:
headers: {
    Authorization: `Bearer ${await getAccessToken?.()}`,
}
Never log or expose JWT tokens in client-side code. They contain sensitive user information and provide access to protected resources.

Available API Endpoints

Get Phone Numbers

Retrieve a list of phone numbers from the inventory:
const phoneNumbers = await ApiService.getPhoneNumbers();
Endpoint: GET /api/phone-numbers Headers:
Authorization: Bearer <jwt_token>
Response:
[
  {
    "id": "123",
    "number": "+1234567890",
    "carrier": "Example Carrier"
  }
]

Get Containers

Retrieve a list of inventory containers:
const containers = await ApiService.getContainers();
Endpoint: GET /api/containers Headers:
Authorization: Bearer <jwt_token>
Response:
[
  {
    "id": "456",
    "name": "Container A",
    "capacity": 100
  }
]

Usage Example

Here’s a complete example of using the API service in a React component:
import React, { useEffect, useState } from 'react';
import useApiService from '@/hooks/useApiService';

const InventoryList: React.FC = () => {
    const ApiService = useApiService();
    const [phoneNumbers, setPhoneNumbers] = useState([]);
    const [containers, setContainers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            try {
                setLoading(true);
                const [phones, boxes] = await Promise.all([
                    ApiService.getPhoneNumbers(),
                    ApiService.getContainers()
                ]);
                setPhoneNumbers(phones);
                setContainers(boxes);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        };

        fetchData();
    }, []);

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;

    return (
        <div>
            <h2>Phone Numbers: {phoneNumbers.length}</h2>
            <h2>Containers: {containers.length}</h2>
        </div>
    );
};

Error Handling

While the current implementation doesn’t include explicit error handling, you should wrap API calls in try-catch blocks:
try {
    const data = await ApiService.getPhoneNumbers();
    // Handle success
} catch (error) {
    if (error.message.includes('Access token provider not set')) {
        // Token provider not configured
    } else if (error.message.includes('Failed to fetch')) {
        // Network error
    } else {
        // Other errors
    }
}
  • 401 Unauthorized: JWT token is invalid or expired
  • 403 Forbidden: User lacks permissions for the requested resource
  • 404 Not Found: API endpoint doesn’t exist
  • 500 Server Error: Backend service is unavailable
  • Network Error: No internet connection or CORS issues

Environment Configuration

Set the API base URL in your .env file:
.env
VITE_API_BASE_URL=https://api.example.com
Vite requires environment variables to be prefixed with VITE_ to be exposed to client-side code.

Extending the API Service

To add new API endpoints, follow this pattern:
static async getUsers(getAccessToken: () => Promise<string> = this.accessTokenProvider) {
    const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users`, {
        headers: {
            Authorization: `Bearer ${await getAccessToken?.()}`,
        },
    });
    return await res.json();
}

Adding POST Requests

static async createContainer(data: any, getAccessToken: () => Promise<string> = this.accessTokenProvider) {
    const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/containers`, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${await getAccessToken?.()}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
    });
    return await res.json();
}

Best Practices

Token Security

Never store JWT tokens in localStorage or sessionStorage. Let Auth0 manage tokens in memory.

Error Boundaries

Wrap API calls in React Error Boundaries to gracefully handle failures.

Loading States

Always show loading indicators during API requests for better UX.

Parallel Requests

Use Promise.all() to fetch multiple endpoints simultaneously.

Next Steps

Authentication

Learn more about JWT token management with Auth0

Dashboard

See how the dashboard uses these API endpoints

Build docs developers (and LLMs) love