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:
- User authorizes your app
- You get authorization code
- Exchange code for access token
- 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:
- Authentication done right from day one
- Error handling isn’t optional
- Respect rate limits (or get blocked)
- Cache strategically to reduce costs
- Test thoroughly before production
- 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:
- Understand authentication thoroughly (OAuth 2.0, API keys, JWT)
- Implement robust error handling with retry logic
- Respect rate limits with proper throttling
- Use webhooks instead of polling when available
- Cache responses to reduce API calls and costs
- Paginate large datasets properly
- Test extensively with mocks and sandbox environments
- Monitor API usage and set up alerts
- Keep secrets in environment variables
- 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?