web-production-saas-starter/next_b2b_starter/docs/05-making-api-requests.md
2025-12-16 18:54:41 +04:00

8.2 KiB

Making API Requests

This guide explains how to make API requests using the API client and repository pattern.

API Client Overview

The app uses a centralized API client that handles:

  • Automatic authentication (adds Bearer token)
  • Token refresh on expiration
  • Error handling
  • Next.js 16 caching options

File: lib/api/api/client/api-client.ts

Basic Usage

Import the API Client

import { apiClient } from '@/lib/api/api/client/api-client';

Make a GET Request

const data = await apiClient.get<ResponseType>('/endpoint');

Make a POST Request

const result = await apiClient.post<ResponseType>('/endpoint', {
  name: 'value',
  count: 42
});

Make a PUT Request

const updated = await apiClient.put<ResponseType>('/endpoint/123', {
  name: 'new value'
});

Make a DELETE Request

await apiClient.delete('/endpoint/123');

API Request Flow

graph LR
    A[Component] --> B[apiClient.get]
    B --> C{Has token?}
    C -->|No| D[token-manager]
    C -->|Yes| E[Add Authorization header]
    D --> E
    E --> F[fetch with token]
    F --> G{200 OK?}
    G -->|Yes| H[Return data]
    G -->|401| I[Refresh token]
    I --> J{Refresh OK?}
    J -->|Yes| F
    J -->|No| K[Logout user]

Handling 401 Errors

When a request returns 401 (unauthorized), the API client automatically:

  1. Attempts to refresh the token
  2. Retries the request with new token
  3. If refresh fails, logs out the user
graph TD
    A[API returns 401] --> B{Token valid?}
    B -->|Expired| C[Call refresh endpoint]
    B -->|Invalid| D[Call refresh endpoint]
    C --> E{Refresh success?}
    D --> E
    E -->|Yes| F[Retry original request]
    E -->|No| G[Clear cookies]
    F --> H{Request success?}
    H -->|Yes| I[Return data]
    H -->|401 again| G
    G --> J[Redirect to /auth]

This happens automatically - you don't need to handle it.

Error Handling

The API client throws ApiError for HTTP errors.

import { apiClient, ApiError } from '@/lib/api/api/client/api-client';

try {
  const data = await apiClient.post('/endpoint', payload);
} catch (error) {
  if (error instanceof ApiError) {
    console.error('Status:', error.status);    // 404, 500, etc.
    console.error('Code:', error.code);        // "HTTP_404", "SESSION_EXPIRED"
    console.error('Message:', error.message);  // Error message
    console.error('Details:', error.details);  // Extra error data
  }
}

Next.js 16 Caching Options

The API client supports Next.js 16 fetch options.

No Cache (Default)

const data = await apiClient.get('/endpoint', {
  cache: 'no-store'  // Don't cache (default for authenticated requests)
});

Force Cache

const data = await apiClient.get('/public-data', {
  cache: 'force-cache'  // Cache indefinitely
});

Revalidate After Time

const data = await apiClient.get('/products', {
  next: {
    revalidate: 3600  // Refresh every hour (3600 seconds)
  }
});

Tag-Based Revalidation

const data = await apiClient.get('/invoices', {
  next: {
    tags: ['invoices', 'financial-data']
  }
});

// Later, revalidate all 'invoices' requests
import { revalidateTag } from 'next/cache';
revalidateTag('invoices');

Repository Pattern

Don't call the API client directly in components. Use repositories instead.

What is a Repository?

A repository is a class that wraps API calls for a specific resource.

Benefits:

  • Centralized API logic
  • Type-safe responses
  • Easy to mock for testing
  • Consistent error handling

Using a Repository

import { profileRepository } from '@/lib/api/api/repositories/profile-repository';

// Server component
export default async function ProfilePage() {
  const session = await requireMemberSession();

  // Use repository
  const profile = await profileRepository.getProfile(session.session_jwt);

  return <div>{profile.name}</div>;
}

Repository Structure

File: lib/api/api/repositories/profile-repository.ts

class ProfileRepository {
  async getProfile(sessionToken?: string) {
    const options = sessionToken
      ? { headers: { Authorization: `Bearer ${sessionToken}` } }
      : undefined;

    return apiClient.get<ProfileResponseDto>('/auth/profile/me', options);
  }
}

export const profileRepository = new ProfileRepository();

Available Repositories

File: lib/api/api/repositories/

  • profile-repository.ts - User profile
  • member-repository.ts - Team members
  • document-repository.ts - Documents
  • rbac-repository.ts - Roles and permissions
  • signup-repository.ts - Organization signup
  • cognitive-repository.ts - AI chat

Skip Authentication

For public endpoints that don't require authentication:

const data = await apiClient.get('/public-data', {
  skipAuth: true
});

This skips adding the Authorization header.

Custom Headers

Add custom headers to any request:

const data = await apiClient.post('/endpoint', payload, {
  headers: {
    'X-Custom-Header': 'value'
  }
});

File Uploads

The API client supports FormData for file uploads.

const formData = new FormData();
formData.append('file', fileBlob);
formData.append('name', 'document.pdf');

const result = await apiClient.post('/upload', formData);

The client automatically:

  • Detects FormData
  • Sets correct Content-Type header
  • Sends as multipart/form-data

Common Patterns

Fetching Data in Server Component

import { requireMemberSession } from '@/lib/auth/stytch/server';
import { invoiceRepository } from '@/lib/api/api/repositories/invoice-repository';

export default async function InvoicesPage() {
  const session = await requireMemberSession();

  // Fetch data using repository
  const invoices = await invoiceRepository.list(session.session_jwt);

  return <InvoiceList invoices={invoices} />;
}

Fetching Data in Client Component

Use React Query hooks (covered in Using Hooks):

'use client';
import { useInvoicesQuery } from '@/lib/hooks/queries/use-invoices-query';

export function InvoiceList() {
  const { data: invoices, isLoading } = useInvoicesQuery();

  if (isLoading) return <div>Loading...</div>;

  return <div>{invoices.map(inv => ...)}</div>;
}

Conditional Requests

export default async function DataPage({ shouldFetchData }) {
  if (!shouldFetchData) {
    return <div>No data</div>;
  }

  const session = await requireMemberSession();
  const data = await dataRepository.fetch(session.session_jwt);

  return <DataView data={data} />;
}

Parallel Requests

Make multiple requests in parallel:

export default async function DashboardPage() {
  const session = await requireMemberSession();

  // Fetch in parallel
  const [profile, invoices, stats] = await Promise.all([
    profileRepository.getProfile(session.session_jwt),
    invoiceRepository.list(session.session_jwt),
    statsRepository.get(session.session_jwt),
  ]);

  return <Dashboard profile={profile} invoices={invoices} stats={stats} />;
}

Testing with Mocks

Mock the API client for testing:

import { apiClient } from '@/lib/api/api/client/api-client';

// Mock implementation
jest.spyOn(apiClient, 'get').mockResolvedValue({
  id: 1,
  name: 'Test'
});

// Test your component
const result = await myFunction();
expect(result.name).toBe('Test');

Key Files

  • lib/api/api/client/api-client.ts - Main API client
  • lib/api/api/client/token-manager.ts - Token management
  • lib/api/api/repositories/ - All repositories
  • lib/api/api/dto/ - TypeScript types for API responses

Best Practices

  1. Always use repositories - Don't call apiClient directly from components
  2. Handle errors - Wrap calls in try/catch
  3. Use TypeScript types - Define response types
  4. Cache wisely - Use next.revalidate for data that changes
  5. Skip auth sparingly - Only for truly public endpoints
  6. Test with mocks - Mock repositories, not the API client

Next Steps

👉 Learn about: Creating Pages