Lyra

Online
The API Integration Guide Every Developer Needs (2019 Edition)
Software Development 13 min read

The API Integration Guide Every Developer Needs (2019 Edition)

S
Squalltec Team May 18, 2019

Why API Integration Matters More Than Ever

2019 Reality: Every business application connects to 5-10+ external services:

  • Payment gateways (Stripe, PayPal)
  • Email services (SendGrid, Mailchimp)
  • CRM systems (Salesforce, HubSpot)
  • Accounting (QuickBooks, Xero)
  • Shipping (FedEx, UPS)
  • And many more…

Your application’s value = How well it integrates with everything else.

This comprehensive guide covers everything you need to know about API integration in 2019, with real code examples and battle-tested practices.

Section 1: Understanding APIs

What is an API?

Application Programming Interface = How software talks to software.

Analogy: Restaurant:

  • Kitchen = backend system
  • Waiter = API
  • Customer = your application

You (customer) don’t go into the kitchen. You tell the waiter (API) what you want. Waiter brings it back.

Types of APIs:

REST (Most Common):

  • Uses HTTP methods (GET, POST, PUT, DELETE)
  • Resource-based URLs
  • Stateless
  • JSON format

Example:

GET https://api.example.com/users/123
POST https://api.example.com/users
PUT https://api.example.com/users/123
DELETE https://api.example.com/users/123

GraphQL (Growing):

  • Query language for APIs
  • Request exactly what you need
  • Single endpoint

Example:

query {
  user(id: 123) {
    name
    email
    orders {
      id
      total
    }
  }
}

SOAP (Legacy):

  • XML-based
  • Formal contracts (WSDL)
  • More complex
  • Still used in enterprise

2019 Trend: REST dominates, GraphQL is growing, SOAP is declining.

Section 2: API Authentication

How APIs Know Who You Are

Method 1: API Keys (Simplest)

const axios = require('axios');

axios.get('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'your_api_key_here'
  }
});

Pros:

  • Simple to implement
  • Easy to understand
  • Good for server-to-server

Cons:

  • If leaked, full access
  • No user-specific permissions
  • No expiration

Use for: Internal APIs, server-to-server communication

Method 2: OAuth 2.0 (Most Secure)

The Flow:

  1. User authorizes your app
  2. You get authorization code
  3. Exchange code for access token
  4. Use token for API calls

Code Example:

// Step 1: Redirect user to authorization URL
const authUrl = `https://api.example.com/oauth/authorize?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=http://yourapp.com/callback&
  response_type=code&
  scope=read_data write_data`;

// User approves, comes back with code

// Step 2: Exchange code for token
const tokenResponse = await axios.post('https://api.example.com/oauth/token', {
  grant_type: 'authorization_code',
  code: req.query.code,
  client_id: 'YOUR_CLIENT_ID',
  client_secret: 'YOUR_CLIENT_SECRET',
  redirect_uri: 'http://yourapp.com/callback'
});

const accessToken = tokenResponse.data.access_token;
const refreshToken = tokenResponse.data.refresh_token;

// Step 3: Use access token
const dataResponse = await axios.get('https://api.example.com/data', {
  headers: {
    'Authorization': `Bearer ${accessToken}`
  }
});

Token Refresh (Important!):

// Access tokens expire (usually 1 hour)
// Use refresh token to get new access token

async function refreshAccessToken(refreshToken) {
  const response = await axios.post('https://api.example.com/oauth/token', {
    grant_type: 'refresh_token',
    refresh_token: refreshToken,
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET'
  });
  
  return response.data.access_token;
}

Pros:

  • Most secure
  • User-specific permissions
  • Tokens expire
  • Can revoke access

Cons:

  • Complex to implement
  • Multiple steps
  • Need to handle refresh

Use for: User-facing APIs, third-party integrations (Google, Facebook, etc.)

Method 3: JWT (JSON Web Tokens)

const jwt = require('jsonwebtoken');

// Create token
const token = jwt.sign(
  { userId: 123, email: 'user@example.com' },
  'YOUR_SECRET_KEY',
  { expiresIn: '1h' }
);

// Verify token
jwt.verify(token, 'YOUR_SECRET_KEY', (err, decoded) => {
  if (err) {
    // Token invalid or expired
  } else {
    // Token valid, decoded contains user data
    console.log(decoded.userId);  // 123
  }
});

Pros:

  • Stateless (no database lookup)
  • Contains user data
  • Cryptographically signed

Cons:

  • Can’t revoke before expiration
  • Token size larger than session ID

Use for: Modern web apps, mobile apps, microservices

Section 3: Making API Requests

HTTP Methods Explained:

GET - Retrieve Data

// Get all users
axios.get('https://api.example.com/users');

// Get specific user
axios.get('https://api.example.com/users/123');

// Get with query parameters
axios.get('https://api.example.com/users', {
  params: {
    page: 1,
    limit: 50,
    status: 'active'
  }
});
// Calls: https://api.example.com/users?page=1&limit=50&status=active

POST - Create Data

// Create new user
axios.post('https://api.example.com/users', {
  name: 'John Doe',
  email: 'john@example.com',
  role: 'customer'
});

PUT - Update Entire Resource

// Replace user completely
axios.put('https://api.example.com/users/123', {
  name: 'John Doe',
  email: 'john.doe@example.com',
  role: 'admin',
  status: 'active'
  // All fields required
});

PATCH - Update Partial Resource

// Update only email
axios.patch('https://api.example.com/users/123', {
  email: 'john.doe@example.com'
  // Only changed fields
});

DELETE - Remove Resource

// Delete user
axios.delete('https://api.example.com/users/123');

Request Headers (Important):

axios.get('https://api.example.com/data', {
  headers: {
    // Authentication
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    
    // API Key (alternative)
    'X-API-Key': 'YOUR_API_KEY',
    
    // Content type (for POST/PUT/PATCH)
    'Content-Type': 'application/json',
    
    // Accept response format
    'Accept': 'application/json',
    
    // Custom headers
    'X-Client-Version': '1.0.0',
    'X-Request-ID': 'unique-request-id'
  }
});

Section 4: Error Handling (Critical!)

HTTP Status Codes:

Success:

  • 200 OK - Request successful
  • 201 Created - Resource created
  • 204 No Content - Successful, no response body

Client Errors (Your Fault):

  • 400 Bad Request - Invalid data sent
  • 401 Unauthorized - No authentication
  • 403 Forbidden - Authenticated but no permission
  • 404 Not Found - Resource doesn’t exist
  • 409 Conflict - Resource conflict (duplicate)
  • 422 Unprocessable Entity - Validation failed
  • 429 Too Many Requests - Rate limit exceeded

Server Errors (Their Fault):

  • 500 Internal Server Error - Server problem
  • 502 Bad Gateway - Upstream server failed
  • 503 Service Unavailable - Server overloaded/down
  • 504 Gateway Timeout - Server timeout

Proper Error Handling:

async function makeAPIRequest() {
  try {
    const response = await axios.get('https://api.example.com/data');
    return response.data;
    
  } catch (error) {
    if (error.response) {
      // Server responded with error status
      const status = error.response.status;
      const data = error.response.data;
      
      switch(status) {
        case 400:
          console.error('Bad request:', data.message);
          // Show user-friendly error
          break;
          
        case 401:
          console.error('Unauthorized - refresh token');
          // Refresh access token and retry
          await refreshToken();
          return makeAPIRequest();  // Retry
          
        case 403:
          console.error('Forbidden - insufficient permissions');
          // Show permission error to user
          break;
          
        case 404:
          console.error('Resource not found');
          // Handle missing resource
          break;
          
        case 429:
          console.error('Rate limit exceeded');
          // Wait and retry with exponential backoff
          await sleep(5000);
          return makeAPIRequest();
          
        case 500:
        case 502:
        case 503:
        case 504:
          console.error('Server error - retry later');
          // Retry with exponential backoff
          return retryWithBackoff(makeAPIRequest);
          
        default:
          console.error('Unknown error:', status, data);
      }
      
    } else if (error.request) {
      // Request made but no response (network issue)
      console.error('Network error - no response received');
      // Retry or show offline message
      
    } else {
      // Something else went wrong
      console.error('Error:', error.message);
    }
    
    throw error;  // Re-throw for caller to handle
  }
}

Exponential Backoff (For Retries):

async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) {
        // Last attempt failed
        throw error;
      }
      
      // Wait before retry (exponentially increasing)
      const delay = baseDelay * Math.pow(2, i);
      console.log(`Retry ${i + 1} after ${delay}ms`);
      await sleep(delay);
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Section 5: Rate Limiting

Why APIs Limit Requests:

  • Prevent abuse
  • Ensure fair usage
  • Protect server resources

Common Limits:

  • Stripe: 100 requests/second
  • Twitter: 900 requests/15 minutes
  • Google Maps: 10 requests/second

Rate Limit Headers:

X-RateLimit-Limit: 1000        // Total allowed per period
X-RateLimit-Remaining: 847     // Requests left
X-RateLimit-Reset: 1558195200  // Unix timestamp when limit resets

Handling Rate Limits:

class APIClient {
  constructor() {
    this.requestQueue = [];
    this.lastRequestTime = 0;
    this.minRequestInterval = 100;  // 100ms between requests = 10 req/sec
  }
  
  async request(url, options) {
    // Wait if needed to respect rate limit
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequestTime;
    
    if (timeSinceLastRequest < this.minRequestInterval) {
      await sleep(this.minRequestInterval - timeSinceLastRequest);
    }
    
    try {
      const response = await axios(url, options);
      this.lastRequestTime = Date.now();
      
      // Check rate limit headers
      const remaining = parseInt(response.headers['x-ratelimit-remaining']);
      const reset = parseInt(response.headers['x-ratelimit-reset']);
      
      if (remaining < 10) {
        console.warn(`Only ${remaining} requests remaining`);
        // Slow down requests
        this.minRequestInterval = 200;
      }
      
      return response.data;
      
    } catch (error) {
      if (error.response?.status === 429) {
        // Rate limit exceeded
        const retryAfter = error.response.headers['retry-after'];
        console.log(`Rate limited. Retry after ${retryAfter} seconds`);
        
        await sleep(retryAfter * 1000);
        return this.request(url, options);  // Retry
      }
      
      throw error;
    }
  }
}

Section 6: Webhooks (Push Instead of Pull)

Problem with Polling:

// Inefficient: Check for new orders every minute
setInterval(async () => {
  const orders = await api.get('/orders?status=new');
  // 99% of the time, no new orders
  // Wasted API calls
}, 60000);

Solution: Webhooks

Server pushes data to you when events happen.

Setting Up Webhooks:

// Your webhook endpoint
app.post('/webhooks/stripe', async (req, res) => {
  const event = req.body;
  
  // Verify webhook signature (important!)
  const signature = req.headers['stripe-signature'];
  let verifiedEvent;
  
  try {
    verifiedEvent = stripe.webhooks.constructEvent(
      req.body,
      signature,
      'YOUR_WEBHOOK_SECRET'
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  // Handle different event types
  switch (verifiedEvent.type) {
    case 'payment_intent.succeeded':
      const paymentIntent = verifiedEvent.data.object;
      await handleSuccessfulPayment(paymentIntent);
      break;
      
    case 'payment_intent.payment_failed':
      const failedPayment = verifiedEvent.data.object;
      await handleFailedPayment(failedPayment);
      break;
      
    case 'customer.subscription.created':
      const subscription = verifiedEvent.data.object;
      await handleNewSubscription(subscription);
      break;
      
    default:
      console.log(`Unhandled event type: ${verifiedEvent.type}`);
  }
  
  // Return 200 to acknowledge receipt
  res.json({ received: true });
});

Webhook Security:

// Verify signature to prevent fake webhooks
function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(payload);
  const computedSignature = hmac.digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computedSignature)
  );
}

Idempotency (Handle Duplicate Webhooks):

// Webhooks may be sent multiple times
// Use idempotency key to prevent duplicate processing

const processedWebhooks = new Set();  // In production: use database

app.post('/webhooks/payment', async (req, res) => {
  const webhookId = req.body.id;
  
  if (processedWebhooks.has(webhookId)) {
    console.log(`Webhook ${webhookId} already processed`);
    return res.json({ received: true });
  }
  
  await processPayment(req.body);
  processedWebhooks.add(webhookId);
  
  res.json({ received: true });
});

Section 7: Pagination

Problem: Large Datasets

// DON'T: Load 1 million records
const allUsers = await api.get('/users');  // Server crashes

Solution: Pagination

Offset-Based Pagination:

// Page 1 (records 1-50)
GET /users?limit=50&offset=0

// Page 2 (records 51-100)
GET /users?limit=50&offset=50

// Page 3 (records 101-150)
GET /users?limit=50&offset=100

Implementation:

async function getAllUsers() {
  const limit = 50;
  let offset = 0;
  let allUsers = [];
  let hasMore = true;
  
  while (hasMore) {
    const response = await api.get(`/users?limit=${limit}&offset=${offset}`);
    allUsers = allUsers.concat(response.data);
    
    if (response.data.length < limit) {
      hasMore = false;  // Last page
    }
    
    offset += limit;
  }
  
  return allUsers;
}

Cursor-Based Pagination (Better for Large Datasets):

// Page 1
GET /users?limit=50

Response:
{
  "data": [...],
  "next_cursor": "eyJpZCI6NTB9"  // Encoded pointer to next page
}

// Page 2
GET /users?limit=50&cursor=eyJpZCI6NTB9

Implementation:

async function getAllUsersWithCursor() {
  let allUsers = [];
  let cursor = null;
  
  while (true) {
    const params = { limit: 50 };
    if (cursor) params.cursor = cursor;
    
    const response = await api.get('/users', { params });
    allUsers = allUsers.concat(response.data.users);
    
    if (!response.data.next_cursor) {
      break;  // No more pages
    }
    
    cursor = response.data.next_cursor;
  }
  
  return allUsers;
}

Section 8: Caching API Responses

Why Cache:

  • Reduce API calls (save money)
  • Faster responses
  • Reduce load on API server

Simple In-Memory Cache:

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 3600 });  // 1 hour default

async function getCachedData(key, fetchFunction) {
  // Check cache first
  const cached = cache.get(key);
  if (cached) {
    console.log('Cache hit');
    return cached;
  }
  
  // Not in cache, fetch from API
  console.log('Cache miss - fetching from API');
  const data = await fetchFunction();
  
  // Store in cache
  cache.set(key, data);
  
  return data;
}

// Usage
const user = await getCachedData(
  `user_${userId}`,
  () => api.get(`/users/${userId}`)
);

Redis Cache (For Production):

const redis = require('redis');
const client = redis.createClient();

async function getCachedDataRedis(key, fetchFunction, ttl = 3600) {
  // Check Redis cache
  const cached = await client.get(key);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Fetch from API
  const data = await fetchFunction();
  
  // Store in Redis with TTL
  await client.setex(key, ttl, JSON.stringify(data));
  
  return data;
}

Cache Invalidation:

// Invalidate when data changes
async function updateUser(userId, updates) {
  await api.patch(`/users/${userId}`, updates);
  
  // Clear cache
  cache.del(`user_${userId}`);
}

Cache Strategies:

1. Cache-Aside (Lazy Loading):

  • Check cache first
  • If miss, fetch and cache
  • Good for: Read-heavy, infrequently changing data

2. Write-Through:

  • Update cache and database together
  • Good for: Write-heavy applications

3. Write-Behind (Write-Back):

  • Update cache immediately
  • Update database asynchronously
  • Good for: High write volume

Section 9: Testing API Integrations

Unit Tests:

const nock = require('nock');  // Mock HTTP requests

describe('API Integration', () => {
  test('should fetch user successfully', async () => {
    // Mock API response
    nock('https://api.example.com')
      .get('/users/123')
      .reply(200, {
        id: 123,
        name: 'John Doe',
        email: 'john@example.com'
      });
    
    // Test your function
    const user = await getUser(123);
    
    expect(user.name).toBe('John Doe');
    expect(user.email).toBe('john@example.com');
  });
  
  test('should handle 404 error', async () => {
    nock('https://api.example.com')
      .get('/users/999')
      .reply(404, { error: 'User not found' });
    
    await expect(getUser(999)).rejects.toThrow('User not found');
  });
  
  test('should retry on 500 error', async () => {
    // First call fails
    nock('https://api.example.com')
      .get('/users/123')
      .reply(500, { error: 'Server error' });
    
    // Second call succeeds
    nock('https://api.example.com')
      .get('/users/123')
      .reply(200, { id: 123, name: 'John' });
    
    const user = await getUserWithRetry(123);
    expect(user.name).toBe('John');
  });
});

Integration Tests:

// Test against actual API (sandbox environment)

describe('Stripe Integration', () => {
  let stripeClient;
  
  beforeAll(() => {
    // Use test API key
    stripeClient = require('stripe')(process.env.STRIPE_TEST_KEY);
  });
  
  test('should create customer', async () => {
    const customer = await stripeClient.customers.create({
      email: 'test@example.com',
      name: 'Test Customer'
    });
    
    expect(customer.email).toBe('test@example.com');
  });
  
  afterAll(async () => {
    // Cleanup: delete test data
  });
});

Section 10: API Integration Checklist

Before Integration:

  • Read API documentation thoroughly
  • Check authentication requirements
  • Understand rate limits
  • Test in sandbox/test environment
  • Review pricing (cost per API call)

During Integration:

  • Use environment variables for secrets
  • Implement proper error handling
  • Add retry logic with exponential backoff
  • Respect rate limits
  • Log all API calls for debugging
  • Validate responses before using
  • Handle pagination correctly
  • Implement caching where appropriate

After Integration:

  • Test error scenarios
  • Monitor API usage
  • Set up alerts for failures
  • Document integration for team
  • Plan for API version updates
  • Regular security audits

Real-World Integration Example: Payment Processing

Complete Stripe Integration:

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

class PaymentService {
  // Create customer
  async createCustomer(email, name) {
    try {
      const customer = await stripe.customers.create({
        email,
        name
      });
      return customer;
    } catch (error) {
      console.error('Stripe customer creation failed:', error);
      throw new Error('Failed to create customer');
    }
  }
  
  // Create payment intent
  async createPaymentIntent(amount, currency, customerId) {
    try {
      const paymentIntent = await stripe.paymentIntents.create({
        amount: amount * 100,  // Convert to cents
        currency,
        customer: customerId,
        automatic_payment_methods: {
          enabled: true
        }
      });
      
      return {
        clientSecret: paymentIntent.client_secret,
        paymentIntentId: paymentIntent.id
      };
    } catch (error) {
      console.error('Payment intent creation failed:', error);
      throw new Error('Failed to create payment intent');
    }
  }
  
  // Confirm payment
  async confirmPayment(paymentIntentId) {
    try {
      const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
      
      if (paymentIntent.status === 'succeeded') {
        // Payment successful
        await this.handleSuccessfulPayment(paymentIntent);
        return { success: true };
      } else {
        return { success: false, status: paymentIntent.status };
      }
    } catch (error) {
      console.error('Payment confirmation failed:', error);
      throw new Error('Failed to confirm payment');
    }
  }
  
  // Handle webhook
  async handleWebhook(rawBody, signature) {
    try {
      const event = stripe.webhooks.constructEvent(
        rawBody,
        signature,
        process.env.STRIPE_WEBHOOK_SECRET
      );
      
      switch (event.type) {
        case 'payment_intent.succeeded':
          await this.handleSuccessfulPayment(event.data.object);
          break;
          
        case 'payment_intent.payment_failed':
          await this.handleFailedPayment(event.data.object);
          break;
          
        case 'charge.refunded':
          await this.handleRefund(event.data.object);
          break;
      }
      
      return { received: true };
    } catch (error) {
      console.error('Webhook handling failed:', error);
      throw error;
    }
  }
  
  async handleSuccessfulPayment(paymentIntent) {
    // Update order status in database
    // Send confirmation email
    // Update inventory
    console.log(`Payment succeeded: ${paymentIntent.id}`);
  }
  
  async handleFailedPayment(paymentIntent) {
    // Notify customer
    // Update order status
    console.log(`Payment failed: ${paymentIntent.id}`);
  }
  
  async handleRefund(charge) {
    // Update order status
    // Send refund notification
    console.log(`Refund processed: ${charge.id}`);
  }
}

module.exports = new PaymentService();

Conclusion: Master API Integration in 2019

APIs are the glue connecting modern applications.

Key Principles:

  1. Authentication done right from day one
  2. Error handling isn’t optional
  3. Respect rate limits (or get blocked)
  4. Cache strategically to reduce costs
  5. Test thoroughly before production
  6. Monitor constantly after launch

Common Mistakes to Avoid:

  • Storing API keys in code
  • No error handling
  • Ignoring rate limits
  • No retry logic
  • Not verifying webhook signatures
  • Loading all data without pagination

Tools to Use:

  • Postman (API testing)
  • ngrok (Local webhook testing)
  • nock (Mocking for tests)
  • axios/fetch (HTTP requests)
  • Redis (Caching)

The API integration landscape evolves fast. Stay updated, test thoroughly, and always have a backup plan.

Key Takeaways:

  1. Understand authentication thoroughly (OAuth 2.0, API keys, JWT)
  2. Implement robust error handling with retry logic
  3. Respect rate limits with proper throttling
  4. Use webhooks instead of polling when available
  5. Cache responses to reduce API calls and costs
  6. Paginate large datasets properly
  7. Test extensively with mocks and sandbox environments
  8. Monitor API usage and set up alerts
  9. Keep secrets in environment variables
  10. Document your integrations for the team

Need help with complex API integration?

We’ve integrated 100+ different APIs: payments, CRMs, accounting, shipping, and more.

[Schedule API Integration Consultation →]

[CONTINUES WITH BLOGS 6-20…]

Due to length, I’ll create blogs 6-20 in a follow-up. Would you like me to continue with the remaining 15 blog posts now?