Skip to main content

Frontend Integration Guide

This comprehensive guide covers integrating the Billbora API with modern frontend applications, including React, Vue, and vanilla JavaScript implementations.

Overview

Billbora provides multiple integration options for frontend applications:

TypeScript SDK

The official Billbora TypeScript SDK provides the easiest way to integrate with our API.

Installation

npm install @billbora/sdk

Basic Setup

import { BillboraClient } from '@billbora/sdk'

const client = new BillboraClient({
  apiKey: 'your-api-key',
  environment: 'production' // or 'sandbox'
})

// Create an invoice
const invoice = await client.invoices.create({
  client: 'client-id',
  line_items: [{
    description: 'Consulting Services',
    quantity: 10,
    unit_price: '150.00'
  }]
})

console.log('Invoice created:', invoice.id)

Configuration Options

interface BillboraClientConfig {
  apiKey: string
  environment?: 'production' | 'sandbox'
  baseURL?: string
  timeout?: number
  retries?: number
  onError?: (error: BillboraError) => void
  onRequest?: (config: RequestConfig) => void
  onResponse?: (response: Response) => void
}

const client = new BillboraClient({
  apiKey: process.env.BILLBORA_API_KEY!,
  environment: 'production',
  timeout: 30000,
  retries: 3,
  onError: (error) => {
    console.error('API Error:', error)
    // Send to error tracking service
  }
})

React Hooks

For React applications, we provide a comprehensive set of hooks built on React Query.

Installation

npm install @billbora/react @tanstack/react-query

Provider Setup

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BillboraProvider } from '@billbora/react'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BillboraProvider apiKey="your-api-key">
        <YourApp />
      </BillboraProvider>
    </QueryClientProvider>
  )
}

Invoice Management

import { useInvoices, useCreateInvoice } from '@billbora/react'

function InvoiceList() {
  const { data: invoices, isLoading } = useInvoices({
    status: 'draft',
    limit: 20
  })
  
  const createInvoice = useCreateInvoice()

  const handleCreateInvoice = async () => {
    try {
      const newInvoice = await createInvoice.mutateAsync({
        client: 'client-id',
        line_items: [{
          description: 'New Service',
          quantity: 1,
          unit_price: '100.00'
        }]
      })
      console.log('Created:', newInvoice.id)
    } catch (error) {
      console.error('Failed to create invoice:', error)
    }
  }

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

  return (
    <div>
      <button onClick={handleCreateInvoice}>
        Create Invoice
      </button>
      {invoices?.results.map(invoice => (
        <div key={invoice.id}>
          {invoice.invoice_number} - ${invoice.total_amount}
        </div>
      ))}
    </div>
  )
}

Real-time Updates

import { useInvoiceSubscription } from '@billbora/react'

function InvoiceDetail({ invoiceId }: { invoiceId: string }) {
  const { data: invoice } = useInvoiceSubscription(invoiceId)

  // Automatically updates when invoice status changes
  return (
    <div>
      <h1>Invoice {invoice?.invoice_number}</h1>
      <p>Status: {invoice?.status}</p>
      <p>Amount: ${invoice?.total_amount}</p>
    </div>
  )
}

Form Integration

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { createInvoiceSchema } from '@billbora/react/schemas'

function CreateInvoiceForm() {
  const createInvoice = useCreateInvoice()
  
  const form = useForm({
    resolver: zodResolver(createInvoiceSchema),
    defaultValues: {
      client: '',
      line_items: [{
        description: '',
        quantity: 1,
        unit_price: '0.00'
      }]
    }
  })

  const onSubmit = async (data) => {
    try {
      await createInvoice.mutateAsync(data)
      form.reset()
    } catch (error) {
      // Error handling is automatic with the hook
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register('client')} placeholder="Client ID" />
      <input {...form.register('line_items.0.description')} placeholder="Description" />
      <input {...form.register('line_items.0.quantity', { valueAsNumber: true })} type="number" />
      <input {...form.register('line_items.0.unit_price')} placeholder="Price" />
      <button type="submit" disabled={form.formState.isSubmitting}>
        Create Invoice
      </button>
    </form>
  )
}

Vue Composables

For Vue 3 applications, we provide reactive composables.

Installation

npm install @billbora/vue @tanstack/vue-query

Setup

// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createBillboraPlugin } from '@billbora/vue'

const app = createApp(App)

app.use(VueQueryPlugin)
app.use(createBillboraPlugin({
  apiKey: 'your-api-key'
}))

app.mount('#app')

Usage in Components

<template>
  <div>
    <div v-if="isLoading">Loading invoices...</div>
    <div v-else>
      <div v-for="invoice in invoices?.results" :key="invoice.id">
        {{ invoice.invoice_number }} - ${{ invoice.total_amount }}
      </div>
    </div>
    <button @click="createNewInvoice" :disabled="isCreating">
      Create Invoice
    </button>
  </div>
</template>

<script setup lang="ts">
import { useInvoices, useCreateInvoice } from '@billbora/vue'

const { data: invoices, isLoading } = useInvoices({
  status: 'draft'
})

const { mutateAsync: createInvoice, isLoading: isCreating } = useCreateInvoice()

const createNewInvoice = async () => {
  try {
    await createInvoice({
      client: 'client-id',
      line_items: [{
        description: 'Vue Service',
        quantity: 1,
        unit_price: '200.00'
      }]
    })
  } catch (error) {
    console.error('Failed:', error)
  }
}
</script>

Direct API Integration

For other frameworks or vanilla JavaScript, you can integrate directly with our REST API.

Fetch API Example

class BillboraAPI {
  constructor(apiKey, baseURL = 'https://api.billbora.com/api/v1') {
    this.apiKey = apiKey
    this.baseURL = baseURL
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`
    const config = {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    }

    const response = await fetch(url, config)
    
    if (!response.ok) {
      const error = await response.json()
      throw new Error(`API Error: ${error.error.message}`)
    }

    return response.json()
  }

  // Invoice methods
  async getInvoices(params = {}) {
    const query = new URLSearchParams(params).toString()
    return this.request(`/invoices/?${query}`)
  }

  async createInvoice(data) {
    return this.request('/invoices/', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  async getInvoice(id) {
    return this.request(`/invoices/${id}/`)
  }

  // Client methods
  async getClients(params = {}) {
    const query = new URLSearchParams(params).toString()
    return this.request(`/clients/?${query}`)
  }

  async createClient(data) {
    return this.request('/clients/', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
}

// Usage
const api = new BillboraAPI('your-api-key')

// Create a client
const client = await api.createClient({
  name: 'Acme Corp',
  email: '[email protected]'
})

// Create an invoice
const invoice = await api.createInvoice({
  client: client.id,
  line_items: [{
    description: 'Consulting',
    quantity: 10,
    unit_price: '150.00'
  }]
})

Axios Example

import axios from 'axios'

// Create axios instance
const billboraAPI = axios.create({
  baseURL: 'https://api.billbora.com/api/v1',
  headers: {
    'Authorization': `Bearer ${process.env.BILLBORA_API_KEY}`,
    'Content-Type': 'application/json'
  }
})

// Add response interceptor for error handling
billboraAPI.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.data?.error) {
      throw new Error(error.response.data.error.message)
    }
    throw error
  }
)

// Usage
async function createInvoice(invoiceData) {
  try {
    const response = await billboraAPI.post('/invoices/', invoiceData)
    return response.data
  } catch (error) {
    console.error('Failed to create invoice:', error.message)
    throw error
  }
}

Authentication Integration

JWT Authentication for Frontend Apps

For frontend applications that need user authentication:
import { createClient } from '@supabase/supabase-js'

class BillboraAuth {
  private supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!
  )
  
  async login(email: string, password: string) {
    // Step 1: Authenticate with Supabase
    const { data, error } = await this.supabase.auth.signInWithPassword({
      email, password
    })
    
    if (error) throw error

    // Step 2: Exchange for Billbora token
    const response = await fetch('/api/v1/users/auth/exchange/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        supabase_token: data.session.access_token
      })
    })

    const tokens = await response.json()
    
    // Store tokens securely
    localStorage.setItem('billbora_tokens', JSON.stringify(tokens))
    
    return tokens
  }
  
  async logout() {
    await this.supabase.auth.signOut()
    localStorage.removeItem('billbora_tokens')
  }
  
  getAccessToken() {
    const tokens = JSON.parse(localStorage.getItem('billbora_tokens') || '{}')
    return tokens.access
  }
}

// Usage with API client
const auth = new BillboraAuth()
const tokens = await auth.login('[email protected]', 'password')

const client = new BillboraClient({
  accessToken: tokens.access
})

Error Handling

Global Error Handler

interface ErrorHandler {
  onValidationError: (errors: Record<string, string[]>) => void
  onNotFound: (resource: string) => void
  onRateLimit: (retryAfter: number) => void
  onUnauthorized: () => void
  onServerError: (error: Error) => void
}

const errorHandler: ErrorHandler = {
  onValidationError: (errors) => {
    // Show field-specific errors in your form
    Object.entries(errors).forEach(([field, messages]) => {
      showFieldError(field, messages[0])
    })
  },
  
  onNotFound: (resource) => {
    showNotification(`${resource} not found`, 'error')
  },
  
  onRateLimit: (retryAfter) => {
    showNotification(`Rate limited. Try again in ${retryAfter} seconds`, 'warning')
  },
  
  onUnauthorized: () => {
    // Redirect to login
    window.location.href = '/login'
  },
  
  onServerError: (error) => {
    showNotification('Something went wrong. Please try again.', 'error')
    console.error('Server error:', error)
  }
}

// Configure client with error handler
const client = new BillboraClient({
  apiKey: 'your-api-key',
  errorHandler
})

Performance Optimization

Caching Strategies

// React Query with custom cache configuration
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: (failureCount, error) => {
        // Don't retry on 4xx errors
        if (error.status >= 400 && error.status < 500) {
          return false
        }
        return failureCount < 3
      }
    }
  }
})

// Custom cache keys for better invalidation
const invoiceKeys = {
  all: ['invoices'] as const,
  lists: () => [...invoiceKeys.all, 'list'] as const,
  list: (filters: string) => [...invoiceKeys.lists(), filters] as const,
  details: () => [...invoiceKeys.all, 'detail'] as const,
  detail: (id: string) => [...invoiceKeys.details(), id] as const,
}

// Usage
const { data: invoices } = useQuery({
  queryKey: invoiceKeys.list(JSON.stringify(filters)),
  queryFn: () => getInvoices(filters)
})

Request Batching

class BatchedBillboraClient {
  private batchQueue: Array<{
    resolve: Function
    reject: Function
    request: any
  }> = []
  
  private batchTimeout: NodeJS.Timeout | null = null

  async batchRequest(request: any) {
    return new Promise((resolve, reject) => {
      this.batchQueue.push({ resolve, reject, request })
      
      if (!this.batchTimeout) {
        this.batchTimeout = setTimeout(() => {
          this.processBatch()
        }, 50) // Batch requests over 50ms
      }
    })
  }

  private async processBatch() {
    const batch = this.batchQueue.splice(0)
    this.batchTimeout = null

    try {
      const response = await fetch('/api/v1/batch/', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          requests: batch.map(item => item.request)
        })
      })

      const results = await response.json()
      
      batch.forEach((item, index) => {
        const result = results[index]
        if (result.error) {
          item.reject(new Error(result.error.message))
        } else {
          item.resolve(result.data)
        }
      })
    } catch (error) {
      batch.forEach(item => item.reject(error))
    }
  }
}

Testing

Mock API Responses

// Create mock client for testing
export class MockBillboraClient extends BillboraClient {
  private mockResponses = new Map()

  setMockResponse(endpoint: string, response: any) {
    this.mockResponses.set(endpoint, response)
  }

  async request(endpoint: string, options?: any) {
    const mockResponse = this.mockResponses.get(endpoint)
    if (mockResponse) {
      if (mockResponse instanceof Error) {
        throw mockResponse
      }
      return mockResponse
    }
    
    return super.request(endpoint, options)
  }
}

// Usage in tests
const mockClient = new MockBillboraClient({ apiKey: 'test' })

mockClient.setMockResponse('/invoices/', {
  results: [
    { id: 'inv_1', invoice_number: 'INV-001', total_amount: '100.00' }
  ],
  count: 1
})

// Test your components with mock data

Integration Tests

import { render, screen, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BillboraProvider } from '@billbora/react'

function createTestWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false }
    }
  })

  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      <BillboraProvider apiKey="test-key">
        {children}
      </BillboraProvider>
    </QueryClientProvider>
  )
}

test('displays invoices', async () => {
  render(<InvoiceList />, { wrapper: createTestWrapper() })
  
  await waitFor(() => {
    expect(screen.getByText('INV-001')).toBeInTheDocument()
  })
})

Next Steps

Support

Need help with your integration?