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
Import the Hook
Import useApiService in your React component: import useApiService from '@/hooks/useApiService' ;
Initialize in Component
Call the hook to get the configured API service: const ApiService = useApiService ();
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:
Valid Token
Expired Token
No Session
If a valid token exists in memory, it’s returned immediately: const token = await getAccessTokenSilently ();
// Returns cached token if not expired
If the token is expired, Auth0 automatically refreshes it using the refresh token: const token = await getAccessTokenSilently ();
// Silently refreshes and returns new token
If no valid session exists, the user is prompted to log in: try {
const token = await getAccessTokenSilently ();
} catch ( error ) {
// User needs to authenticate
}
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:
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