Skip to main content

Overview

SIGEAC communicates with a Laravel backend API using Axios. All API interactions are configured with authentication, error handling, and proper TypeScript types.

Axios Configuration

The Axios instance is configured in lib/axios.ts:
lib/axios.ts
import axios from 'axios'
import Cookies from 'js-cookie'

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  withCredentials: true,
  headers: {
    "skip_zrok_interstitial": true,
  },
})

// Request interceptor - Add auth token
axiosInstance.interceptors.request.use((config) => {
  const token = Cookies.get('auth_token')
  
  if (token) {
    const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`
    config.headers.Authorization = authHeader
  }
  
  return config
})

// Response interceptor - Handle 401 errors
axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      console.warn("⚠️ Sesión inválida: Redirigiendo al login...")
      
      Cookies.remove('auth_token')
      Cookies.remove('jwt')
      
      if (typeof window !== 'undefined') {
        window.location.href = '/login?session=expired'
      }
    }
    
    return Promise.reject(error)
  }
)

export default axiosInstance
See: lib/axios.ts:1

Environment Configuration

Set the API base URL in .env.local:
.env.local
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
# Or production URL:
# NEXT_PUBLIC_API_BASE_URL=https://api.sigeac.com/api
The NEXT_PUBLIC_ prefix exposes the variable to the browser. Only use it for public URLs.

API Patterns

GET Requests

Fetch a collection:
const fetchClients = async (company: string): Promise<Client[]> => {
  const { data } = await axiosInstance.get(`/${company}/clients`)
  return data
}

POST Requests

Create a new item:
const createClient = async (
  company: string,
  clientData: CreateClientData
): Promise<Client> => {
  const { data } = await axiosInstance.post(
    `/${company}/clients`,
    clientData
  )
  return data
}

PATCH/PUT Requests

Update specific fields:
const updateClient = async (
  company: string,
  id: string,
  updates: Partial<Client>
): Promise<Client> => {
  const { data } = await axiosInstance.patch(
    `/${company}/clients/${id}`,
    updates
  )
  return data
}

DELETE Requests

const deleteClient = async (company: string, id: string): Promise<void> => {
  await axiosInstance.delete(`/${company}/clients/${id}`)
}

Multi-Tenant URL Structure

SIGEAC uses company slugs in URLs for multi-tenancy:
/{company}/clients          # List clients for company
/{company}/clients/{id}     # Get specific client
/hangar74/flights           # Hangar74's flights
/transmandu/flights         # Transmandu's flights
Example:
const { selectedCompany } = useCompanyStore()
const company = selectedCompany?.slug // "hangar74" or "transmandu"

const { data } = await axiosInstance.get(`/${company}/clients`)

Response Types

Single Resource Response

interface Client {
  id: number
  name: string
  email: string
  phone: string
  created_at: string
  updated_at: string
}

// API Response
const response = await axiosInstance.get<Client>('/clients/1')
const client: Client = response.data

Collection Response

// Simple array
const response = await axiosInstance.get<Client[]>('/clients')
const clients: Client[] = response.data

Paginated Response

interface PaginatedResponse<T> {
  data: T[]
  current_page: number
  last_page: number
  per_page: number
  total: number
  from: number
  to: number
}

const response = await axiosInstance.get<PaginatedResponse<Client>>(
  '/clients?page=1'
)

const {
  data: clients,
  current_page,
  last_page,
  total
} = response.data

Error Response

interface ApiErrorResponse {
  message: string
  errors?: Record<string, string[]> // Validation errors
}

try {
  await axiosInstance.post('/clients', data)
} catch (error) {
  if (axios.isAxiosError(error)) {
    const errorData = error.response?.data as ApiErrorResponse
    console.error(errorData.message)
    console.error(errorData.errors)
  }
}

Error Handling

Global Error Handling

The response interceptor handles 401 errors globally:
axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login
      window.location.href = '/login?session=expired'
    }
    return Promise.reject(error)
  }
)

Component-Level Error Handling

import { useGetClients } from '@/hooks/general/clientes/useGetClients'

export function ClientList() {
  const { data, error, isError } = useGetClients(company)
  
  if (isError) {
    return (
      <Alert variant="destructive">
        <AlertTitle>Error</AlertTitle>
        <AlertDescription>
          {error.message || 'Failed to load clients'}
        </AlertDescription>
      </Alert>
    )
  }
  
  return <div>{/* Render clients */}</div>
}

Validation Errors

Laravel returns validation errors in a specific format:
interface ValidationError {
  message: string
  errors: {
    [field: string]: string[]
  }
}

// Example response:
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email field is required."],
    "name": ["The name must be at least 3 characters."]
  }
}
Handle in your component:
import { AxiosError } from 'axios'

type ValidationErrorResponse = {
  message: string
  errors: Record<string, string[]>
}

try {
  await createClient.mutateAsync({ company, data })
} catch (error) {
  if (axios.isAxiosError(error)) {
    const errorResponse = error.response?.data as ValidationErrorResponse
    
    if (errorResponse.errors) {
      // Set form errors
      Object.entries(errorResponse.errors).forEach(([field, messages]) => {
        form.setError(field as any, {
          message: messages[0]
        })
      })
    }
  }
}

Authentication Flow

Login

contexts/AuthContext.tsx
import { useMutation } from '@tanstack/react-query'
import axiosInstance from '@/lib/axios'
import { createCookie } from '@/lib/cookie'
import { createSession } from '@/lib/session'

const loginMutation = useMutation({
  mutationFn: async (credentials: { login: string; password: string }) => {
    const response = await axiosInstance.post<User>('/login', credentials, {
      headers: { 'Content-Type': 'application/json' },
    })

    const token = response.headers['authorization']
    if (!token) throw new Error('No se recibió token de autenticación')

    createCookie("auth_token", token)
    await createSession(response.data.id)

    return response.data
  },
  onSuccess: async (userData) => {
    await fetchUser()
    queryClient.invalidateQueries({ queryKey: ['user'] })
    router.push('/inicio')
    toast.success('¡Bienvenido!')
  },
  onError: (err: Error) => {
    const axiosError = err as AxiosError<ApiErrorResponse>
    const errorMessage = axiosError.response?.data?.message || 'Error al iniciar sesión'
    
    setError(errorMessage)
    toast.error('Error', { description: errorMessage })
  },
})
See: contexts/AuthContext.tsx:137

Logout

const logout = async () => {
  try {
    await axiosInstance.post('/logout')
  } catch (error) {
    console.error('Logout error:', error)
  } finally {
    Cookies.remove('auth_token')
    queryClient.clear()
    router.push('/login')
  }
}

Get Current User

const fetchUser = async (): Promise<User> => {
  const { data } = await axiosInstance.get<User>('/user')
  return data
}

File Uploads

Single File Upload

const uploadCertificate = async (
  company: string,
  articleId: string,
  file: File
) => {
  const formData = new FormData()
  formData.append('certificate', file)
  formData.append('article_id', articleId)
  
  const { data } = await axiosInstance.post(
    `/${company}/articles/${articleId}/certificates`,
    formData,
    {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    }
  )
  
  return data
}

Multiple Files Upload

const uploadMultipleDocuments = async (
  company: string,
  files: File[]
) => {
  const formData = new FormData()
  
  files.forEach((file, index) => {
    formData.append(`documents[${index}]`, file)
  })
  
  const { data } = await axiosInstance.post(
    `/${company}/documents/batch`,
    formData,
    {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    }
  )
  
  return data
}

With Progress Tracking

import { useState } from 'react'

const [uploadProgress, setUploadProgress] = useState(0)

const uploadWithProgress = async (file: File) => {
  const formData = new FormData()
  formData.append('file', file)
  
  const { data } = await axiosInstance.post('/upload', formData, {
    onUploadProgress: (progressEvent) => {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / (progressEvent.total || 1)
      )
      setUploadProgress(percentCompleted)
    },
  })
  
  return data
}

Download Files

Download as Blob

const downloadCertificate = async (
  company: string,
  certificateId: string
) => {
  const response = await axiosInstance.get(
    `/${company}/certificates/${certificateId}/download`,
    {
      responseType: 'blob',
    }
  )
  
  // Create download link
  const url = window.URL.createObjectURL(new Blob([response.data]))
  const link = document.createElement('a')
  link.href = url
  link.setAttribute('download', 'certificate.pdf')
  document.body.appendChild(link)
  link.click()
  link.remove()
}

Get File URL

const getDocumentUrl = (path: string) => {
  const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace('/api', '')
  return `${baseUrl}/storage/${path}`
}

// Usage
<img src={getDocumentUrl(article.image)} alt={article.part_number} />

Request Cancellation

Cancel on Component Unmount

import { useEffect } from 'react'
import axios from 'axios'

useEffect(() => {
  const source = axios.CancelToken.source()
  
  const fetchData = async () => {
    try {
      const { data } = await axiosInstance.get('/data', {
        cancelToken: source.token,
      })
      setData(data)
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log('Request cancelled')
      }
    }
  }
  
  fetchData()
  
  return () => {
    source.cancel('Component unmounted')
  }
}, [])

Best Practices

Don’t create new axios instances:
// ✗ Bad
import axios from 'axios'
axios.get('http://api.example.com/data')

// ✓ Good
import axiosInstance from '@/lib/axios'
axiosInstance.get('/data')
Always define response types:
// ✗ Bad
const { data } = await axiosInstance.get('/clients')

// ✓ Good
const { data } = await axiosInstance.get<Client[]>('/clients')
Different errors need different handling:
  • 401: Redirect to login (handled globally)
  • 403: Show access denied message
  • 404: Show not found message
  • 422: Show validation errors
  • 500: Show server error message
Never hardcode API URLs:
// ✗ Bad
const baseURL = 'http://localhost:8000/api'

// ✓ Good
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL

Testing API Integration

Mock Axios in Tests

import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

const mock = new MockAdapter(axiosInstance)

// Mock GET request
mock.onGet('/clients').reply(200, [
  { id: 1, name: 'Client 1' },
  { id: 2, name: 'Client 2' },
])

// Mock POST request
mock.onPost('/clients').reply(201, {
  id: 3,
  name: 'New Client',
})

// Test
const { data } = await axiosInstance.get('/clients')
expect(data).toHaveLength(2)

Next Steps

Data Fetching

Learn data fetching patterns with React Query

Custom Hooks

Create reusable API hooks

Build docs developers (and LLMs) love