cloud, webdev

How to Use ngrok and LocalTunnel: Expose Your Local APIs to the World

Intro

As developers, we often face the challenge of testing our local applications with external services, webhooks, or mobile devices. Whether you’re developing APIs that need to communicate with AWS/GCP/Azure services, testing webhook integrations, or simply want to demo your work from different devices, exposing your localhost to the internet becomes essential.

This guide will walk you through two popular solutions: ngrok and LocalTunnel, showing you how to securely expose your local development server to the world.

What Are Tunneling Services?

Tunneling services create a secure tunnel from a public endpoint to your local machine, allowing external services to reach your development server without complex network configuration or deployment.

Common Use Cases

  • Testing webhooks from third-party services (Stripe, GitHub, etc.) — You can connect your local code directly and debug it more efficiently.
  • Sharing your work-in-progress with clients or team members — Instead of pushing it to some remote server. Useful in all the cases, where you are still ‘not ready’.
  • Testing mobile applications that need to connect to your local API — A must have in almost all cases.
  • Integrating with AWS services that require publicly accessible endpoints
  • Cross-device testing and debugging

Quick Reference Commands

ngrok:

# Basic tunnel
ngrok http 3000

# HTTPS tunnel
ngrok http https://localhost:3000

# Custom subdomain
ngrok http 3000 --subdomain=myapp

# With authentication
ngrok http 3000 --basic-auth="user:pass"

LocalTunnel:

# Basic tunnel
lt --port 3000

# Custom subdomain
lt --port 3000 --subdomain myapp

# Specific local host
lt --port 3000 --local-host 127.0.0.1


Method 1: Using ngrok

ngrok is the most popular tunneling service, offering both free and paid tiers with additional features.

Installation

Option 1: Download from website

# Visit https://ngrok.com/download and download for your OS
# Extract and add to PATH

Option 2: Using package managers

# macOS (Homebrew)
brew install ngrok/ngrok/ngrok

# Windows (Chocolatey)
choco install ngrok

# Linux (Snap)
sudo snap install ngrok

Setup and Authentication

# Sign up at https://ngrok.com and get your auth token
ngrok authtoken YOUR_AUTH_TOKEN_HERE

Basic Usage Examples

Example 1: Simple Express.js API Server

First, let’s create a basic API server:

// server.js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

// Sample API endpoints
app.get('/api/health', (req, res) => {
    res.json({ status: 'OK', timestamp: new Date().toISOString() });
});

app.get('/api/users', (req, res) => {
    res.json({
        users: [
            { id: 1, name: 'John Doe', email: 'john@example.com' },
            { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
        ]
    });
});

app.post('/api/webhook', (req, res) => {
    console.log('Webhook received:', req.body);
    res.json({ message: 'Webhook processed successfully' });
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

Start your server:

node server.js

Expose with ngrok:

# Basic HTTP tunnel
ngrok http 3000

# With custom subdomain (paid feature)
ngrok http 3000 --subdomain=myapp

# HTTPS only
ngrok http https://localhost:3000

# With basic auth
ngrok http 3000 --basic-auth="username:password"

Advanced ngrok Configuration

Create an ngrok configuration file (~/.ngrok2/ngrok.yml):

version: "2"
authtoken: YOUR_AUTH_TOKEN

tunnels:
  api:
    addr: 3000
    proto: http
    subdomain: myapi
    auth: "user:pass"
    
  database:
    addr: 5432
    proto: tcp
    
  frontend:
    addr: 8080
    proto: http
    host_header: localhost:8080

Start multiple tunnels:

# Start specific tunnel
ngrok start api

# Start multiple tunnels
ngrok start api frontend

# Start all tunnels
ngrok start --all

Real-World Example: AWS Lambda Integration

Here’s how to test AWS Lambda locally and expose it for webhook testing:

// lambda-local.js
const express = require('express');
const app = express();

app.use(express.json());

// Simulate AWS Lambda function locally
const lambdaHandler = async (event, context) => {
    try {
        const { body, headers, httpMethod, path } = event;
        
        // Your Lambda logic here
        const result = {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                message: 'Lambda function executed successfully',
                input: body,
                method: httpMethod,
                path: path
            })
        };
        
        return result;
    } catch (error) {
        return {
            statusCode: 500,
            body: JSON.stringify({ error: error.message })
        };
    }
};

// Express wrapper for Lambda
app.all('/lambda/*', async (req, res) => {
    const event = {
        body: JSON.stringify(req.body),
        headers: req.headers,
        httpMethod: req.method,
        path: req.path,
        queryStringParameters: req.query
    };
    
    const context = {
        functionName: 'local-test',
        requestId: `local-${Date.now()}`
    };
    
    const result = await lambdaHandler(event, context);
    
    res.status(result.statusCode)
        .set(result.headers)
        .send(result.body);
});

app.listen(3001, () => {
    console.log('Lambda simulator running on http://localhost:3001');
    console.log('Run: ngrok http 3001');
});

Method 2: Using LocalTunnel

LocalTunnel is a free, open-source alternative to ngrok that’s simpler to set up but has fewer features.

Installation

# Install globally via npm
npm install -g localtunnel

Basic Usage

# Expose port 3000
lt --port 3000

# With custom subdomain
lt --port 3000 --subdomain myapp

# With local host specification
lt --port 3000 --local-host localhost

LocalTunnel with Express Server

// server-with-lt.js
const express = require('express');
const localtunnel = require('localtunnel');

const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/api/status', (req, res) => {
    res.json({
        status: 'running',
        tunnel: process.env.TUNNEL_URL || 'not set',
        timestamp: new Date().toISOString()
    });
});

app.post('/api/data', (req, res) => {
    console.log('Received data:', req.body);
    res.json({
        received: req.body,
        processed: true
    });
});

const server = app.listen(PORT, async () => {
    console.log(`Server running on http://localhost:${PORT}`);
    
    try {
        // Create tunnel programmatically
        const tunnel = await localtunnel({ port: PORT });
        console.log(`Tunnel URL: ${tunnel.url}`);
        process.env.TUNNEL_URL = tunnel.url;
        
        tunnel.on('close', () => {
            console.log('Tunnel closed');
        });
        
    } catch (error) {
        console.error('Tunnel error:', error);
    }
});

Mobile Development Integration

React Native Example

// config.js
const isDevelopment = __DEV__;

export const API_BASE_URL = isDevelopment 
    ? 'https://abc123.ngrok.io/api'  // Your ngrok URL
    : 'https://your-production-api.com/api';

// api.js
import { API_BASE_URL } from './config';

export const fetchUsers = async () => {
    try {
        const response = await fetch(`${API_BASE_URL}/users`);
        return await response.json();
    } catch (error) {
        console.error('API Error:', error);
        throw error;
    }
};

export const sendWebhook = async (data) => {
    try {
        const response = await fetch(`${API_BASE_URL}/webhook`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
        });
        return await response.json();
    } catch (error) {
        console.error('Webhook Error:', error);
        throw error;
    }
};

Flutter Example

// api_service.dart
class ApiService {
  static const String _baseUrl = 'https://abc123.ngrok.io/api';
  
  static Future<Map<String, dynamic>> getUsers() async {
    final response = await http.get(
      Uri.parse('$_baseUrl/users'),
      headers: {'Content-Type': 'application/json'},
    );
    
    if (response.statusCode == 200) {
      return json.decode(response.body);
    } else {
      throw Exception('Failed to fetch users');
    }
  }
  
  static Future<void> sendData(Map<String, dynamic> data) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/data'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode(data),
    );
    
    if (response.statusCode != 200) {
      throw Exception('Failed to send data');
    }
  }
}

AWS Integration Examples

Testing S3 Upload Callbacks

// s3-webhook-handler.js
const express = require('express');
const multer = require('multer');
const AWS = require('aws-sdk');

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

// Configure AWS (use environment variables)
const s3 = new AWS.S3({
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    region: process.env.AWS_REGION
});

app.use(express.json());

// Upload endpoint that triggers S3 upload
app.post('/api/upload', upload.single('file'), async (req, res) => {
    try {
        const params = {
            Bucket: process.env.S3_BUCKET_NAME,
            Key: `uploads/${Date.now()}-${req.file.originalname}`,
            Body: req.file.buffer,
            ContentType: req.file.mimetype,
            // This will trigger S3 event notification to your ngrok URL
            Metadata: {
                'webhook-url': `${req.protocol}://${req.get('host')}/api/s3-callback`
            }
        };
        
        const result = await s3.upload(params).promise();
        
        res.json({
            success: true,
            url: result.Location,
            key: result.Key
        });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// S3 event callback handler
app.post('/api/s3-callback', (req, res) => {
    console.log('S3 Event received:', JSON.stringify(req.body, null, 2));
    
    // Process S3 event
    const records = req.body.Records || [];
    
    records.forEach(record => {
        if (record.eventName.startsWith('s3:ObjectCreated')) {
            console.log(`File uploaded: ${record.s3.object.key}`);
            // Trigger additional processing here
        }
    });
    
    res.json({ processed: true });
});

app.listen(3000, () => {
    console.log('S3 webhook handler running on port 3000');
    console.log('Expose with: ngrok http 3000');
});

Security Considerations

Best Practices

  1. Use Authentication: Always protect sensitive endpoints
# ngrok with basic auth
ngrok http 3000 --basic-auth="username:password"

  1. Whitelist IP Addresses: Restrict access to known IPs
const allowedIPs = ['192.168.1.100', '10.0.0.1'];

app.use((req, res, next) => {
    const clientIP = req.ip || req.connection.remoteAddress;
    if (!allowedIPs.includes(clientIP)) {
        return res.status(403).json({ error: 'Access denied' });
    }
    next();
});

  1. Environment-Specific Configuration:
const config = {
    development: {
        allowTunnel: true,
        requireAuth: false
    },
    production: {
        allowTunnel: false,
        requireAuth: true
    }
};

const currentConfig = config[process.env.NODE_ENV || 'development'];

Troubleshooting Common Issues

ngrok Issues

Connection Refused:

# Check if your local server is running
curl http://localhost:3000

# Verify port number
netstat -tlnp | grep :3000

Tunnel Not Working:

# Check ngrok status
ngrok status

# Restart with verbose logging
ngrok http 3000 --log=stdout

LocalTunnel Issues

Installation Problems:

# Clear npm cache
npm cache clean --force

# Reinstall
npm uninstall -g localtunnel
npm install -g localtunnel

Connection Issues:

# Try different subdomain
lt --port 3000 --subdomain myapp-$(date +%s)

# Use different server
lt --port 3000 --host https://serverless.social

Performance Considerations

Optimizing for Production-Like Testing

// performance-middleware.js
const compression = require('compression');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const app = express();

// Security middleware
app.use(helmet());

// Compression for better performance over tunnel
app.use(compression());

// Rate limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limit each IP to 100 requests per windowMs
});

app.use('/api/', limiter);

// Request logging for debugging
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
    next();
});

Conclusion

Both ngrok and LocalTunnel are powerful tools for exposing your local development environment to the world. ngrok offers more features and better reliability, while LocalTunnel provides a free, simple alternative for basic use cases.

Choose ngrok when you need:

  • Professional features (custom domains, authentication, etc.)
  • Reliable uptime for important demos
  • Advanced configuration options
  • Integration with CI/CD pipelines

Choose LocalTunnel when you need:

  • A quick, free solution
  • Simple HTTP tunneling
  • Open-source tooling
  • Programmatic tunnel creation

Remember to always consider security implications when exposing local services to the internet, and never expose sensitive data or production credentials through development tunnels.

Happy tunneling! 🚀


Discover more from Ido Green

Subscribe to get the latest posts sent to your email.

Standard