Skip to content

Latest commit

Β 

History

History
515 lines (405 loc) Β· 13.5 KB

File metadata and controls

515 lines (405 loc) Β· 13.5 KB

API Integration Guide & Production Checklist

πŸ“‹ Overview

This document provides a comprehensive guide for API integration between the Next.js frontend and Express backend, including implementation patterns, testing strategies, and a production checklist.


πŸ—οΈ Folder Structure

src/
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ api/                        # Core API layer
β”‚   β”‚   β”œβ”€β”€ index.ts               # Public API exports
β”‚   β”‚   β”œβ”€β”€ client.ts              # HTTP client with interceptors
β”‚   β”‚   β”œβ”€β”€ config.ts              # Configuration & token storage
β”‚   β”‚   └── types.ts               # API-related TypeScript types
β”‚   β”‚
β”‚   β”œβ”€β”€ services/                   # Service layer (API wrappers)
β”‚   β”‚   β”œβ”€β”€ index.ts               # Service exports
β”‚   β”‚   β”œβ”€β”€ auth.service.ts        # Authentication endpoints
β”‚   β”‚   β”œβ”€β”€ courses.service.ts     # Courses endpoints
β”‚   β”‚   └── health.service.ts      # Health check endpoints
β”‚   β”‚
β”‚   └── hooks/                      # React hooks for data fetching
β”‚       β”œβ”€β”€ index.ts               # Hook exports
β”‚       └── use-api.ts             # useApi, useMutation, useInfiniteScroll
β”‚
β”œβ”€β”€ types/                          # Global TypeScript types
β”‚   β”œβ”€β”€ user.ts                    # User types
β”‚   └── profile.ts                 # Profile types
β”‚
└── __tests__/                      # Test files
    β”œβ”€β”€ setup.ts                   # Jest setup
    β”œβ”€β”€ api/
    β”‚   β”œβ”€β”€ api-client.test.ts     # Unit tests for API client
    β”‚   └── integration.test.ts    # E2E integration tests
    └── services/
        └── services.test.ts       # Service unit tests

πŸ”Œ Implementation Examples

1. GET Request Example

import { api, ApiResponse } from '@/lib/api';
import { Course } from '@/lib/services';

// Define response type
type CoursesResponse = ApiResponse<{
  courses: Course[];
  total: number;
}>;

// Make the request
async function fetchCourses(page: number = 1): Promise<CoursesResponse> {
  return api.get<CoursesResponse>('/courses', { 
    page: String(page), 
    limit: '10' 
  });
}

// Usage in component
const { data, success } = await fetchCourses(1);
if (success) {
  console.log('Courses:', data.courses);
}

2. POST Request Example

import { api, ApiResponse, ApiError } from '@/lib/api';

// Define request and response types
interface LoginRequest {
  email: string;
  password: string;
}

interface LoginData {
  user: { id: string; name: string; email: string };
  token: string;
}

// Make the request with proper typing
async function login(credentials: LoginRequest): Promise<LoginData> {
  const response = await api.post<ApiResponse<LoginData>>(
    '/auth/login',
    credentials
  );
  
  if (!response.success) {
    throw new Error(response.message || 'Login failed');
  }
  
  return response.data;
}

// Usage with error handling
try {
  const { user, token } = await login({ 
    email: 'user@example.com', 
    password: 'password123' 
  });
  console.log('Logged in as:', user.name);
} catch (error) {
  if (error instanceof ApiError) {
    if (error.isAuthError()) {
      console.error('Invalid credentials');
    } else if (error.isValidationError()) {
      console.error('Validation errors:', error.errors);
    }
  }
}

3. Error Handling Patterns

import { ApiError } from '@/lib/api';

async function handleApiCall<T>(
  apiCall: () => Promise<T>,
  options?: {
    onSuccess?: (data: T) => void;
    onError?: (error: ApiError) => void;
    showToast?: boolean;
  }
): Promise<T | null> {
  const { onSuccess, onError, showToast = true } = options || {};
  
  try {
    const result = await apiCall();
    onSuccess?.(result);
    return result;
  } catch (error) {
    if (error instanceof ApiError) {
      // Handle different error types
      if (error.isAuthError()) {
        // Redirect to login
        window.location.href = '/login';
      } else if (error.isForbiddenError()) {
        showToast && toast.error('You do not have permission');
      } else if (error.isValidationError()) {
        showToast && toast.error(error.message);
      } else if (error.isServerError()) {
        showToast && toast.error('Server error. Please try again later.');
      } else if (error.isNetworkError) {
        showToast && toast.error('Network error. Check your connection.');
      }
      
      onError?.(error);
    }
    return null;
  }
}

4. Using Hooks for Loading States

import { useApi, useMutation } from '@/lib/hooks';
import { coursesService, authService } from '@/lib/services';

// GET with loading state
function CoursesPage() {
  const { data, isLoading, error, execute } = useApi(
    () => coursesService.getCourses(),
    { immediate: true }
  );

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  
  return (
    <div>
      {data?.data.courses.map(course => (
        <CourseCard key={course._id} course={course} />
      ))}
      <button onClick={execute}>Refresh</button>
    </div>
  );
}

// POST with mutation
function LoginForm() {
  const { mutate, isLoading, error } = useMutation(
    (data: { email: string; password: string }) => 
      authService.login(data.email, data.password),
    {
      onSuccess: (response) => {
        tokenStorage.setToken(response.data.token);
        router.push('/dashboard');
      },
      onError: (error) => {
        toast.error(error.message);
      },
    }
  );

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    mutate({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

πŸ§ͺ Running Tests

Test Commands

# Run all tests
npm test

# Run unit tests only (no API calls)
npm run test:unit

# Run integration/E2E tests (requires live backend)
npm run test:e2e

# Run with coverage report
npm run test:coverage

# Run in watch mode during development
npm run test:watch

Expected Test Outputs

βœ… Successful Test Run

PASS  src/__tests__/api/api-client.test.ts
PASS  src/__tests__/services/services.test.ts
PASS  src/__tests__/api/integration.test.ts

πŸ“Š API Status Report:
──────────────────────────────────────────────────
API URL: https://alpha-squad-back-end.vercel.app/api
Reachable: βœ… Yes
Avg Latency: 234ms

Endpoint Status:
  βœ… Health: 156ms
  βœ… Courses: 312ms
  βœ… Auth: 234ms
──────────────────────────────────────────────────

Test Suites: 3 passed, 3 total
Tests:       25 passed, 25 total

❌ Failed Test Run (Backend Unreachable)

FAIL  src/__tests__/api/integration.test.ts
  ● Backend Connectivity β€Ί should be able to reach the backend API
    
    Expected: true
    Received: false
    
    Network error: Unable to reach the server.

πŸ“Š API Status Report:
──────────────────────────────────────────────────
API URL: https://alpha-squad-back-end.vercel.app/api
Reachable: ❌ No
Error: Network error

Endpoint Status:
  ❌ Health: timeout
  ❌ Courses: timeout
  ❌ Auth: timeout
──────────────────────────────────────────────────

βœ… Production Checklist

Environment Configuration

Item Status Notes
NEXT_PUBLIC_API_URL is set βœ… Points to production backend
No hard-coded URLs in codebase βœ… All URLs use env vars
No secrets in NEXT_PUBLIC_* vars βœ… Only public config exposed
.env.local is in .gitignore βœ… Secrets not committed

API Client

Item Status Notes
Uses environment variable for base URL βœ… apiConfig.baseUrl
Automatic token injection βœ… Via request interceptor
Request timeout configured βœ… 30 second default
Retry logic for GET requests βœ… 3 retries with backoff
Proper error handling βœ… ApiError class
TypeScript types for all responses βœ… Defined in types.ts

Error Handling

Item Status Notes
Network errors caught βœ… isNetworkError flag
Auth errors (401) handle logout βœ… Token cleared on 401
Validation errors have details βœ… errors array in ApiError
Server errors (5xx) show friendly message βœ… Generic user message
Timeout errors handled βœ… 408 status code

Type Safety

Item Status Notes
All API responses typed βœ… ApiResponse<T> wrapper
Request payloads typed βœ… Service method signatures
Error types defined βœ… ApiError, ApiValidationError
No any types in API layer βœ… Strict TypeScript

Testing

Item Status Notes
Unit tests for API client βœ… api-client.test.ts
Service layer tests βœ… services.test.ts
Integration tests βœ… integration.test.ts
Tests fail on backend unreachable βœ… Connectivity assertions
Coverage thresholds set βœ… 70% minimum

🚫 Common Mistakes & How We Prevent Them

❌ Hard-Coded URLs

Bad:

const response = await fetch('http://localhost:5000/api/courses');

Good (How we do it):

// URL comes from environment variable
const response = await api.get('/courses');
// Internally uses: apiConfig.baseUrl + '/courses'

Prevention: The api client reads from NEXT_PUBLIC_API_URL and there are no hard-coded URLs anywhere in the API layer.


❌ Missing Error Handling

Bad:

const data = await fetch('/api/data').then(r => r.json());
// If fetch fails or response is not OK, this silently fails

Good (How we do it):

try {
  const data = await api.get('/data');
} catch (error) {
  if (error instanceof ApiError) {
    // Structured error with status code, message, and details
    console.error(`[${error.statusCode}] ${error.message}`);
  }
}

Prevention: The API client wraps all responses in try/catch and throws structured ApiError objects with status codes and validation details.


❌ Untyped API Responses

Bad:

const response = await fetch('/api/users');
const data = await response.json(); // data is 'any'
data.users.map(u => u.name); // No type safety

Good (How we do it):

interface UsersResponse {
  users: Array<{ id: string; name: string; email: string }>;
}

const response = await api.get<ApiResponse<UsersResponse>>('/users');
response.data.users.map(u => u.name); // βœ… Fully typed

Prevention: All service methods have explicit return types, and the ApiResponse<T> wrapper ensures consistent typing.


❌ No Loading States

Bad:

const [data, setData] = useState([]);
useEffect(() => {
  fetchData().then(setData); // No loading indicator
}, []);

Good (How we do it):

const { data, isLoading, error } = useApi(
  () => fetchData(),
  { immediate: true }
);

if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <DataList items={data} />;

Prevention: The useApi and useMutation hooks provide built-in isLoading, error, and isSuccess states.


❌ Token Not Sent with Requests

Bad:

// Forgot to add Authorization header
const response = await fetch('/api/protected');

Good (How we do it):

// Token is automatically added by the request interceptor
const response = await api.get('/protected');
// Headers include: Authorization: Bearer <token>

Prevention: The API client has a request interceptor that automatically adds the Authorization header if a token exists in storage.


❌ No Request Timeout

Bad:

// No timeout - could hang forever
const response = await fetch('/api/slow-endpoint');

Good (How we do it):

// Built-in 30 second timeout
const response = await api.get('/slow-endpoint');

// Or custom timeout
const response = await api.get('/slow-endpoint', { timeout: 5000 });

Prevention: All requests have a default 30 second timeout configured in apiConfig.timeout, with AbortController handling.


πŸ“š Quick Reference

Import Patterns

// Main API client
import { api, ApiError, apiConfig, tokenStorage } from '@/lib/api';

// Services
import { authService, coursesService, healthService } from '@/lib/services';

// Hooks
import { useApi, useMutation, useInfiniteScroll } from '@/lib/hooks';

// Types
import type { ApiResponse, PaginatedResponse, RequestState } from '@/lib/api';

Environment Variables

# Required
NEXT_PUBLIC_API_URL=https://alpha-squad-back-end.vercel.app/api

# Optional
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name

πŸ”— Related Documentation