Expose your localhost to the internet using a free‑tier tunnel service
Welcome to another official UVF IT build guide. In the next 45 minutes you'll spin up a tiny Node.js app and connect it to a public tunnel so anyone on the web can hit your local endpoint, just like Portr does.
We'll use the open‑source localtunnel package as our cloud relay – no paid accounts, no API keys. By the end you’ll have a working clone you can extend with custom routes or authentication.
localtunnel.localtunnel.me relay – performance is limited compared to the full Portr service.localtunnel may be blocked; disable the proxy or use a direct internet connection.Create a fresh folder for your clone, initialise it as an npm project, and pull in the two packages we need: Express for the web server and localtunnel for the relay. This gives us a clean starting point and guarantees all dependencies are recorded in package.json.
Running the commands below will also generate a lockfile so future installs are reproducible.
mkdir portr-clone && cd portr-clone
npm init -y
npm install express localtunnelNow we write the tiny Express app that will be exposed through the tunnel. It listens on a local port, defines a single GET route at /status that returns a JSON payload, and finally starts the server. Keeping the code minimal makes it easy to see exactly what will be reachable from the internet once the tunnel is active.
index.js
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.get('/status', (req, res) => {
res.json({ service: 'portr-clone', status: 'online' })
})
app.listen(PORT, () => {
console.log(`🚀 App listening on http://localhost:${PORT}`)
})Now that the Express server is up, we want a public address without manual steps. By invoking localtunnel from Node we can spin up a tunnel as soon as the app begins listening, capture the URL, and print it for the developer. This keeps the workflow one‑command and mirrors how the original Portr CLI works.
tunnel.js
const localtunnel = require('localtunnel');
// Called after the HTTP server starts listening.
async function startTunnel(port) {
try {
const tunnel = await localtunnel({ port });
console.log('Public URL:', tunnel.url);
// Keep the tunnel alive for the lifetime of the process.
tunnel.on('close', () => console.log('Tunnel closed'));
} catch (err) {
console.error('Failed to start tunnel:', err);
}
}
module.exports = { startTunnel };We need a single command that launches both the server and the tunnel script. npm-run-all (or the built‑in concurrently package) lets us start multiple Node processes in parallel, forwarding their stdout so we can see the public URL immediately. Once both are running, a quick curl or browser hit proves the tunnel works.
# Install the helper if you haven't already
npm install --save-dev concurrently
# Add a script to package.json (you can paste this into the "scripts" section)
# "start": "concurrently \"node index.js\" \"node tunnel.js\""
# Then run the combined command
npm run startAdding more endpoints is just a matter of defining another Express handler; the tunnel code stays untouched because it only cares about the port. Below we add a /time route that returns the current server timestamp in JSON. This demonstrates how easy it is to grow the API without re‑engineering the tunneling logic.
index.js
const express = require('express');
const { startTunnel } = require('./tunnel');
const app = express();
const PORT = 3000;
// Existing route
app.get('/hello', (req, res) => {
res.json({ message: 'Hello from your local tunnel!' });
});
// New optional route – returns server time
app.get('/time', (req, res) => {
res.json({ time: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`App listening on http://localhost:${PORT}`);
// Kick off the tunnel as soon as the server is ready
startTunnel(PORT);
});| Check | Command / Why it matters |
|---|---|
| Server starts without errors | Terminal shows App listening on http://localhost:3000 after running the concurrent command. |
| Public URL printed | Terminal displays Public URL: https://<random>.loca.lt from tunnel.js. |
| Endpoint reachable from the internet | Run curl https://<random>.loca.lt/hello and receive the JSON response. |
| Second route works (if added) | Visit https://<random>.loca.lt/time in a browser and see the timestamp JSON. |
You now have a functional HTTP tunnel clone. Experiment by adding authentication middleware, HTTPS redirection, or swapping localtunnel for another free relay like ngrok (free tier) to explore different features.