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.

S

StalkTechie

Author

February 23, 2026
32 views

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!

Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment