Chapter 1: useApi

The useApi hook simplifies API calls with automatic JWT authentication, token refresh, and error management.

Configuration

The API is configured in appConfig.js:

// src/appConfig.js
export const config = {
    api: {
        prefixUrl: import.meta.env.VITE_API_URL,
        timeout: 30000,
        debug: import.meta.env.DEV,
        paths: {
            login: "login",
            logout: "logout",
            refresh: "refresh"
        }
    }
};

Returned Structure

import { useApi } from '@cap-rel/smartcommon';

const api = useApi();
Method Description
api.user Logged in user object
api.login(body) User login
api.logout() Logout
api.entities() Get available entities
api.device(body) Register a device
api.public ky instance for public requests
api.private ky instance for authenticated requests
api.get(url, options) Authenticated GET shortcut
api.post(url, options) Authenticated POST shortcut
api.put(url, options) Authenticated PUT shortcut
api.patch(url, options) Authenticated PATCH shortcut
api.del(url, options) Authenticated DELETE shortcut

Authentication

Login

import { useApi, useNavigation } from '@cap-rel/smartcommon';

function LoginPage() {
    const api = useApi();
    const nav = useNavigation();

    const handleLogin = async (email, password) => {
        try {
            const user = await api.login({
                login: email,
                password: password,
                rememberMe: true
            });

            console.log('Logged in:', user);
            nav.navigate('/');
        } catch (error) {
            console.error('Login error:', error);
            alert('Incorrect credentials');
        }
    };

    return (
        <form onSubmit={(e) => {
            e.preventDefault();
            const fd = new FormData(e.target);
            handleLogin(fd.get('email'), fd.get('password'));
        }}>
            <input name="email" type="email" placeholder="Email" />
            <input name="password" type="password" placeholder="Password" />
            <button type="submit">Login</button>
        </form>
    );
}

Logout

function LogoutButton() {
    const api = useApi();
    const nav = useNavigation();

    const handleLogout = async () => {
        await api.logout();
        nav.navigate('/login');
    };

    return (
        <button onClick={handleLogout}>Logout</button>
    );
}

Access Logged User

useApi directly exposes the user object:

const api = useApi();

if (api.user) {
    console.log('Logged in:', api.user.login);
    console.log('Token expires at:', api.user.tokenExpiry);
}

Public Requests

For endpoints without authentication:

const api = useApi();

// GET
const info = await api.public.get('public/info').json();

// POST
const result = await api.public.post('public/contact', {
    json: { name: 'John', message: 'Hello' }
}).json();

Authenticated Requests

For JWT-protected endpoints:

const api = useApi();

// GET - List
const items = await api.private.get('items').json();

// GET - Detail
const item = await api.private.get(`items/${id}`).json();

// POST - Create
const newItem = await api.private.post('items', {
    json: { label: 'New product', price: 29.99 }
}).json();

// PUT - Update
const updated = await api.private.put(`items/${id}`, {
    json: { label: 'Modified product' }
}).json();

// DELETE - Delete
await api.del(`items/${id}`);

Shortcut Methods

In addition to api.private.get(...).json(), useApi exposes shortcuts that automatically handle JSON parsing and errors:

const api = useApi();

// GET - directly returns parsed data
const items = await api.get('items');

// POST
const created = await api.post('items', { json: { label: 'New' } });

// PUT
const updated = await api.put(`items/${id}`, { json: data });

// PATCH
const patched = await api.patch(`items/${id}`, { json: { status: 1 } });

// DELETE
await api.del(`items/${id}`);

Raw Option

For retrieving binary data (images, Excel files, PDFs...):

const response = await api.get(`temp-file/${token}/binary`, { raw: true });
const blob = await response.blob();

Silent Option

Removes automatic error notification:

const data = await api.get('optional-endpoint', { silent: true });

Complete Example: CRUD

import { useEffect } from 'react';
import { Page, Block, List, ListItem, Button, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates, useNavigation } from '@cap-rel/smartcommon';

export const ProductsPage = () => {
    const api = useApi();
    const nav = useNavigation();

    const st = useStates({
        initialStates: {
            products: [],
            loading: true,
            error: null
        }
    });

    // Load products
    useEffect(() => {
        loadProducts();
    }, []);

    const loadProducts = async () => {
        st.set('loading', true);
        st.set('error', null);

        try {
            const data = await api.private.get('products').json();
            st.set('products', data.products);
        } catch (err) {
            st.set('error', err.message);
        } finally {
            st.set('loading', false);
        }
    };

    // Delete a product
    const deleteProduct = async (id) => {
        if (!confirm('Delete this product?')) return;

        try {
            await api.del(`products/${id}`);
            // Remove from local list
            st.set('products', st.get('products').filter(p => p.id !== id));
        } catch (err) {
            alert('Error: ' + err.message);
        }
    };

    if (st.get('loading')) {
        return <Page title="Products"><Spinner /></Page>;
    }

    if (st.get('error')) {
        return (
            <Page title="Products">
                <Block>
                    <p>Error: {st.get('error')}</p>
                    <Button onClick={loadProducts}>Retry</Button>
                </Block>
            </Page>
        );
    }

    return (
        <Page title="Products" onRefresh={loadProducts}>
            <Block>
                <Button onClick={() => nav.navigate('/products/new')}>
                    New Product
                </Button>
            </Block>

            <Block>
                <List>
                    {st.get('products').map(product => (
                        <ListItem
                            key={product.id}
                            title={product.label}
                            subtitle={`${product.price} €`}
                            onClick={() => nav.navigate(`/products/${product.id}`)}
                            actions={
                                <Button
                                    size="sm"
                                    variant="danger"
                                    onClick={(e) => {
                                        e.stopPropagation();
                                        deleteProduct(product.id);
                                    }}
                                >
                                    Delete
                                </Button>
                            }
                        />
                    ))}
                </List>
            </Block>
        </Page>
    );
};

Error Handling

const api = useApi();

const createProduct = async (data) => {
    try {
        const result = await api.private.post('products', { json: data }).json();
        return { success: true, data: result };
    } catch (error) {
        // HTTP error
        if (error.response) {
            const status = error.response.status;
            const body = await error.response.json().catch(() => ({}));

            if (status === 400) {
                return { success: false, error: 'Invalid data', details: body };
            }
            if (status === 401) {
                return { success: false, error: 'Session expired' };
            }
            if (status === 403) {
                return { success: false, error: 'Access denied' };
            }
            if (status === 404) {
                return { success: false, error: 'Resource not found' };
            }
            if (status >= 500) {
                return { success: false, error: 'Server error' };
            }
        }

        // Network error
        return { success: false, error: 'Connection error' };
    }
};

Global Error Handling (onApiError)

Configure a global callback in appConfig to centralize API error handling:

// src/appConfig.js
import toast from 'react-hot-toast';

export const config = {
    api: {
        prefixUrl: import.meta.env.VITE_API_URL,
        onApiError: (message) => {
            toast.error(message);
        },
    },
};

When a request fails via shortcut methods (get, post, put, patch, del), useApi:

  • Extracts the error message from the JSON body (error or message field)
  • Calls onApiError with this message (unless silent: true)
  • Also detects application errors (HTTP 200 with error field in the body)

Automatic Features

JWT Token Management

  • Access token is automatically added to api.private requests
  • Token is automatically refreshed before expiration
  • On 401 error, a refresh is automatically attempted

Automatic Headers

Each request includes:

  • Authorization: Bearer <accessToken> (private requests)
  • X-DEVICEID: <uuid> (device identification)
  • Content-Type: application/json

Circuit Breaker

Protection against cascade requests:

  • Temporary blocking after multiple failures
  • Automatic internet connection detection

Query Parameters

Query String

// GET /products?category=electronics&limit=10
const products = await api.private.get('products', {
    searchParams: {
        category: 'electronics',
        limit: 10
    }
}).json();

Custom Headers

const data = await api.private.get('items', {
    headers: {
        'X-Custom-Header': 'value'
    }
}).json();

Custom Timeout

const data = await api.private.get('slow-endpoint', {
    timeout: 60000  // 60 seconds
}).json();

Key Points to Remember

  1. api.login() automatically handles token storage
  2. api.private automatically adds authentication
  3. api.public for endpoints without auth
  4. End with .json() to parse the response
  5. The refresh token is handled automatically
  6. api.get/post/put/patch/del : shortcuts with automatic error handling
  7. api.user : direct access to the logged in user
  8. onApiError : centralizes error notification
  9. { raw: true } for binary files
  10. { silent: true } to suppress error notifications

Back to Module | Next Chapter: State Management ->