Mastering Third-Party API Integration in Node.js: A Practical Guide
Learn how to integrate external APIs in your Node.js applications. From REST to GraphQL, authentication to rate limiting, this guide covers everything with real-world examples.
StalkTechie
Author
Third-Party API Integration in Node.js: From Basics to Production
Modern applications rarely live in isolation. Whether you're adding payment processing, weather data, or AI features, integrating third-party APIs is essential. In this guide, we'll build practical integrations for StalkTechie that you can use in your own projects.
Before We Begin
Make sure you have:
- Node.js v20+ installed
- Basic JavaScript/Node.js knowledge
- API keys from services we'll use (free tiers available)
š” Quick tip: We'll use real APIs. Sign up for free accounts at OpenWeather and GitHub to follow along.
1. Project Setup
Initialize the Project
mkdir stalktechie-api-integration
cd stalktechie-api-integration
npm init -y
npm install axios express dotenv cors helmet
npm install -D nodemon
What we're installing: axios (HTTP client), express (web server), dotenv (environment variables), cors/helmet (security), nodemon (development).
Project Structure
stalktechie-api-integration/
āāā src/
ā āāā clients/ # API client wrappers
ā āāā services/ # Business logic
ā āāā routes/ # Express routes
ā āāā utils/ # Helper functions
āāā .env
āāā .gitignore
āāā server.js
Create the directories:
mkdir -p src/{clients,services,routes,utils}
touch server.js .env .gitignore
Basic Server
server.js:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'OK', message: 'API Integration Demo' });
});
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message });
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
.env file:
PORT=3000
OPENWEATHER_API_KEY=your_key_here
GITHUB_TOKEN=your_token_here
2. Building a Reusable HTTP Client
Instead of making raw HTTP calls everywhere, let's build a client that handles common concerns like logging and error handling.
src/utils/httpClient.js:
const axios = require('axios');
class HttpClient {
constructor(baseURL, defaultHeaders = {}, timeout = 10000) {
this.client = axios.create({
baseURL,
timeout,
headers: { 'Content-Type': 'application/json', ...defaultHeaders }
});
this.client.interceptors.request.use((config) => {
console.log(`Request: ${config.method.toUpperCase()} ${config.url}`);
config.metadata = { startTime: Date.now() };
return config;
});
this.client.interceptors.response.use(
(response) => {
const duration = Date.now() - response.config.metadata.startTime;
console.log(`Response received in ${duration}ms`);
return response.data;
},
(error) => {
if (error.response) {
error.apiError = {
status: error.response.status,
message: error.response.data?.message || error.message,
data: error.response.data
};
} else if (error.request) {
error.apiError = { status: 503, message: 'No response from server' };
} else {
error.apiError = { status: 500, message: error.message };
}
return Promise.reject(error);
}
);
}
async get(url, params = {}) { return this.client.get(url, { params }); }
async post(url, data = {}) { return this.client.post(url, data); }
}
module.exports = HttpClient;
3. Example 1: OpenWeatherMap API
Let's integrate a weather API to get current conditions and forecasts.
Weather Client
src/clients/weatherClient.js:
const HttpClient = require('../utils/httpClient');
class WeatherClient extends HttpClient {
constructor() {
const apiKey = process.env.OPENWEATHER_API_KEY;
if (!apiKey) throw new Error('OPENWEATHER_API_KEY is required');
super('https://api.openweathermap.org/data/2.5');
this.apiKey = apiKey;
}
async getCurrentWeather(city, country = '', units = 'metric') {
try {
const query = country ? `${city},${country}` : city;
const data = await this.get('/weather', {
q: query,
appid: this.apiKey,
units,
lang: 'en'
});
return {
location: data.name,
country: data.sys.country,
temperature: {
current: data.main.temp,
feels_like: data.main.feels_like,
min: data.main.temp_min,
max: data.main.temp_max
},
humidity: data.main.humidity,
weather: {
main: data.weather[0].main,
description: data.weather[0].description,
icon: data.weather[0].icon
},
wind: data.wind.speed
};
} catch (error) {
throw this.handleError(error, city);
}
}
handleError(error, location) {
if (error.apiError?.status === 404)
return new Error(`Location '${location}' not found`);
if (error.apiError?.status === 401)
return new Error('Invalid API key');
return error;
}
}
module.exports = WeatherClient;
Weather Service with Business Logic
src/services/weatherService.js:
const WeatherClient = require('../clients/weatherClient');
class WeatherService {
constructor() {
this.client = new WeatherClient();
}
async getWeatherForCity(city, country = '') {
try {
const weather = await this.client.getCurrentWeather(city, country);
// Add practical recommendations
const recommendations = this.getRecommendations(weather);
return { success: true, data: { ...weather, recommendations } };
} catch (error) {
return { success: false, error: error.message };
}
}
getRecommendations(weather) {
const temp = weather.temperature.current;
const recs = [];
if (temp < 0) recs.push('Extreme cold - stay indoors');
else if (temp < 10) recs.push('Cold - bring a jacket');
else if (temp < 20) recs.push('Mild - light jacket recommended');
else if (temp < 30) recs.push('Pleasant - enjoy outdoors');
else recs.push('Hot - stay hydrated');
if (weather.weather.main.toLowerCase().includes('rain'))
recs.push('Rain expected - bring umbrella');
return recs;
}
}
module.exports = WeatherService;
Weather Routes
src/routes/weather.routes.js:
const router = require('express').Router();
const WeatherService = require('../services/weatherService');
const weatherService = new WeatherService();
router.get('/city/:city', async (req, res, next) => {
try {
const { city } = req.params;
const { country } = req.query;
const result = await weatherService.getWeatherForCity(city, country);
if (!result.success) return res.status(400).json(result);
res.json(result);
} catch (error) { next(error); }
});
module.exports = router;
Add to server.js:
app.use('/api/weather', require('./src/routes/weather.routes'));
Test it:
curl http://localhost:3000/api/weather/city/London
4. Example 2: GitHub API with Caching
Let's add caching to reduce API calls and improve performance.
Install cache package:
npm install node-cache
GitHub Client
src/clients/githubClient.js:
const HttpClient = require('../utils/httpClient');
class GitHubClient extends HttpClient {
constructor() {
const token = process.env.GITHUB_TOKEN;
if (!token) throw new Error('GITHUB_TOKEN is required');
super('https://api.github.com', {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json'
});
}
async getUserProfile(username) {
try {
const data = await this.get(`/users/${username}`);
return {
login: data.login,
name: data.name,
avatar: data.avatar_url,
bio: data.bio,
publicRepos: data.public_repos,
followers: data.followers,
following: data.following
};
} catch (error) {
throw this.handleError(error, username);
}
}
async getUserRepos(username) {
try {
const data = await this.get(`/users/${username}/repos`, {
sort: 'updated',
per_page: 10
});
return data.map(repo => ({
name: repo.name,
description: repo.description,
language: repo.language,
stars: repo.stargazers_count,
forks: repo.forks_count,
url: repo.html_url
}));
} catch (error) {
throw this.handleError(error, username);
}
}
handleError(error, context) {
if (error.apiError?.status === 404)
return new Error(`User '${context}' not found`);
if (error.apiError?.status === 403 && error.response?.headers?.['x-ratelimit-remaining'] === '0') {
const reset = new Date(parseInt(error.response.headers['x-ratelimit-reset']) * 1000);
return new Error(`Rate limit exceeded. Resets at ${reset.toLocaleTimeString()}`);
}
return error;
}
}
module.exports = GitHubClient;
GitHub Service with Caching
src/services/githubService.js:
const NodeCache = require('node-cache');
const GitHubClient = require('../clients/githubClient');
class GitHubService {
constructor() {
this.client = new GitHubClient();
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minutes cache
}
async getUserProfile(username) {
const cacheKey = `github:user:${username}`;
const cached = this.cache.get(cacheKey);
if (cached) {
console.log('Cache hit');
return { success: true, data: cached, cached: true };
}
try {
const data = await this.client.getUserProfile(username);
this.cache.set(cacheKey, data);
return { success: true, data, cached: false };
} catch (error) {
return { success: false, error: error.message };
}
}
async getUserRepos(username) {
const cacheKey = `github:repos:${username}`;
const cached = this.cache.get(cacheKey);
if (cached) return { success: true, data: cached, cached: true };
try {
const data = await this.client.getUserRepos(username);
this.cache.set(cacheKey, data);
return { success: true, data, cached: false };
} catch (error) {
return { success: false, error: error.message };
}
}
clearCache(username) {
const keys = this.cache.keys();
const userKeys = keys.filter(k => k.includes(username));
userKeys.forEach(k => this.cache.del(k));
return { cleared: userKeys.length };
}
}
module.exports = GitHubService;
GitHub Routes
src/routes/github.routes.js:
const router = require('express').Router();
const GitHubService = require('../services/githubService');
const githubService = new GitHubService();
router.get('/users/:username', async (req, res, next) => {
try {
const result = await githubService.getUserProfile(req.params.username);
if (!result.success) return res.status(404).json(result);
res.set('X-Cache', result.cached ? 'HIT' : 'MISS');
res.json(result);
} catch (error) { next(error); }
});
router.get('/users/:username/repos', async (req, res, next) => {
try {
const result = await githubService.getUserRepos(req.params.username);
if (!result.success) return res.status(404).json(result);
res.json(result);
} catch (error) { next(error); }
});
router.delete('/cache/:username', (req, res) => {
const result = githubService.clearCache(req.params.username);
res.json({ message: `Cleared ${result.cleared} cache entries` });
});
module.exports = router;
Add to server.js:
app.use('/api/github', require('./src/routes/github.routes'));
Test it:
curl http://localhost:3000/api/github/users/octocat
curl http://localhost:3000/api/github/users/octocat/repos
5. Handling Webhooks Securely
Webhooks allow APIs to send real-time data. Let's implement a secure webhook handler.
Install Stripe (for webhook example):
npm install stripe
Add to .env: STRIPE_WEBHOOK_SECRET=whsec_your_secret
Webhook Handler
src/webhooks/stripeWebhook.js:
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
class StripeWebhookHandler {
constructor() {
this.webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
}
constructEvent(payload, signature) {
try {
return stripe.webhooks.constructEvent(payload, signature, this.webhookSecret);
} catch (err) {
throw new Error(`Webhook signature verification failed: ${err.message}`);
}
}
async handleEvent(event) {
console.log(`Webhook received: ${event.type}`);
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePaymentSucceeded(event.data.object);
break;
case 'payment_intent.payment_failed':
await this.handlePaymentFailed(event.data.object);
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
}
async handlePaymentSucceeded(paymentIntent) {
console.log('Payment succeeded:', {
id: paymentIntent.id,
amount: paymentIntent.amount / 100
});
// Update order status, send email, etc.
}
async handlePaymentFailed(paymentIntent) {
console.log('Payment failed:', paymentIntent.last_payment_error);
// Notify user, update order status, etc.
}
}
module.exports = StripeWebhookHandler;
Webhook Route
src/routes/webhook.routes.js:
const router = require('express').Router();
const StripeWebhookHandler = require('../webhooks/stripeWebhook');
router.post('/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
if (!sig) return res.status(400).send('Missing signature');
try {
const handler = new StripeWebhookHandler();
const event = handler.constructEvent(req.body, sig);
// Process asynchronously
setImmediate(() => handler.handleEvent(event).catch(console.error));
res.json({ received: true });
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
}
});
module.exports = router;
Add to server.js:
app.use('/webhooks', require('./src/routes/webhook.routes'));
6. Best Practices & Common Mistakes
ā Do This
- Use environment variables for all API keys
- Implement caching to reduce API calls
- Add retry logic for transient failures
- Validate webhook signatures for security
- Set reasonable timeouts (5-10 seconds)
- Log API calls for debugging
ā Avoid This
- Hardcoding API keys in source code
- Ignoring errors - always handle them
- Blocking the event loop with sync code
- Forgetting rate limits - they will hit you
- Exposing raw API responses to clients
Common Errors & Solutions
| Error | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Invalid API key | Check env variables, regenerate key |
| 429 Too Many Requests | Rate limit hit | Add delays, implement backoff |
| ECONNREFUSED | Service down | Add retries, circuit breaker |
Simple Retry Logic
async function callWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
if (error.response?.status >= 500 || error.code === 'ECONNREFUSED') {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error; // Don't retry client errors
}
}
}
}
What We've Built
In this tutorial, we built:
- ā A reusable HTTP client with error handling
- ā Weather API integration with business logic
- ā GitHub API client with caching (5-minute TTL)
- ā Secure webhook handler with signature verification
- ā Best practices for production-ready integrations
š Next Steps
- Add rate limiting middleware to your own API
- Implement a circuit breaker pattern
- Add request queuing for high-volume APIs
- Write tests using nock or jest
The code is available on GitHub. Use it as a starting point for your own integrations.
Happy coding!