How to put a express server in Browser
While exploring WebRTC, I had a simple thought:
WebRTC communication feels a lot like HTTP β so why not abstract it as an HTTP-like service?
If you compare a typical Node.js HTTP server with WebRTC data channel communication, theyβre actually quite similar:
- Both listen for incoming messages/events
- Both process requests
- Both send responses back
π A Quick Comparisonβ
HTTP Server (Node.js)β
const http = require('http');
const PORT = 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
WebRTC DataChannelβ
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel('my-channel');
dataChannel.onopen = () => {
console.log('Data channel opened');
};
dataChannel.onmessage = (event) => {
console.log('received data:', event.data);
};
let remoteChannel = null;
peerConnection.ondatachannel = (event) => {
remoteChannel = event.channel;
remoteChannel.onmessage = (event) => {
console.log('received remote message:', event.data);
// reply
remoteChannel.send('gotcha');
};
};
From this, it becomes obvious:
π The DataChannel is essentially an event-driven transport layer, just like HTTP.
So I asked:
Can I build an HTTP-like server abstraction on top of WebRTC?
The answer turned out to be: yes.
π§± Step 1: Define a Message Protocolβ
Unlike HTTP, we donβt need to support complex content types.
We can keep things simple and use JSON.
Important: All peers must follow the same message structure.
export type PeerMessage = {
id: string;
src: string;
dest: string;
method: string;
path: string;
body: Record<string, any>;
headers: Record<string, any>;
status: string;
guid?: string;
timestamp: number | string;
metadata?: Record<string, any>;
scope: 'request' | 'response';
};
π§ Step 2: Build a Router (Like Express)β
If youβve ever read the source code of Express or Koa, youβll notice:
A router is surprisingly simple β you can build one in under 100 lines.
I built a slightly enhanced version with:
- Routing (GET / POST)
- Middleware support
- Error handling
- Route caching
- Performance monitoring
Hereβs the core idea:
export class PeerRouter {
protected routes = [];
protected middleware = [];
get(path: string, callback: RequestCallbackFn) {
this.routes.push({ path, method: 'GET', callback });
}
post(path: string, callback: RequestCallbackFn) {
this.routes.push({ path, method: 'POST', callback });
}
use(handler: RequestCallbackFn) {
this.middleware.push(handler);
}
async run(conn: DataConnection, message: PeerMessage) {
const route = this.routes.find((r) => r.path === message.path);
if (!route) {
console.warn(`No route for ${message.method} ${message.path}`);
return;
}
for (const mw of this.middleware) {
await mw(conn, message);
}
await route.callback(conn, message);
}
}
βοΈ Step 3: Create a PeerServerβ
This is where everything comes together.
- Handle incoming requests
- Match routes
- Track request/response lifecycle
class PeerServer {
private router = new PeerRouter();
private requests = new Map();
use(handler: RequestCallbackFn) {
this.router.use(handler);
}
addRoute(route: RouteItem) {
this.router.add(route);
}
async onMessage(conn: DataConnection, message: PeerMessage) {
if (message.scope === 'request') {
return this.router.run(conn, message);
}
if (message.scope === 'response') {
const cb = this.requests.get(message.id);
if (cb) cb(message);
}
}
}
π§ͺ Step 4: Use It Like Expressβ
Now comes the fun part.
We can write WebRTC logic just like Express:
const server = new PeerServer();
server.use(async (conn, message, next) => {
if (message.headers['x-device-id'] !== 'xx') {
throw new Error('Invalid device id');
}
return next();
});
server.get('/ping', (conn, message) => {
conn.send({
...message,
body: { message: 'ok' },
status: 200,
});
});
Hook it into WebRTC:
dataChannel.onmessage = (event) => {
server.onMessage(dataChannel, event.data);
};
peerConnection.ondatachannel = (event) => {
const remoteChannel = event.channel;
remoteChannel.onmessage = (event) => {
server.onMessage(remoteChannel, event.data);
};
};
π― The Resultβ
You now have:
An Express-like server running entirely inside the browser, powered by WebRTC.
No ports. No servers. No traditional backend.
π Real-world Usageβ
This architecture is already powering π https://egosapp.com
All P2P communication in EGOS is built on top of this system.
It lets me:
- Write WebRTC logic like Express APIs
- Treat devices as distributed endpoints
- Build a decentralized file network
π§ Final Thoughtβ
HTTP changed how we build applications by standardizing communication.
WebRTC opens the door to something new:
Peer-to-peer applications that feel like traditional client-server systems β but without servers.
If you're interested, feel free to try it out or share feedback.