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
- Use Authentication: Always protect sensitive endpoints
# ngrok with basic auth
ngrok http 3000 --basic-auth="username:password"
- 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();
});
- 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.