Introduction
A Digital Twin is a faithful virtual representation of a physical object, updated in real-time using sensor data. In IoT, it enables simulation, prediction, and optimization of equipment behavior without risking the real world. For example, a Digital Twin of an industrial temperature sensor mirrors its variations, alerts on anomalies, and tests virtual scenarios.
This beginner tutorial guides you through creating a basic Digital Twin for a temperature sensor with Node.js, Express for the API, and Socket.io for real-time updates. Imagine a physical thermostat: its twin shows the current temperature, simulates fluctuations, and allows virtual control. By the end, you'll have a functional, scalable prototype for real-world cases like Industry 4.0. Why is this crucial in 2026? Digital Twins reduce maintenance costs by 20-30% according to Gartner, and this setup is ideal for portfolios or rapid prototypes. (142 words)
Prerequisites
- Node.js 20+ installed (download here)
- Code editor like VS Code
- Basic JavaScript knowledge (variables, functions, async)
- Terminal (cmd or bash)
- No IoT experience required
Project Initialization
mkdir digital-twin-app
cd digital-twin-app
npm init -y
npm install express socket.io cors
npm install -D nodemonThese commands create a project folder, initialize npm, and install the essential dependencies: Express for the HTTP server, Socket.io for bidirectional real-time communication, CORS for cross-origin requests. Nodemon is a dev tool that auto-restarts the server on code changes.
package.json Configuration
{
"name": "digital-twin-app",
"version": "1.0.0",
"description": "Digital Twin simple avec Node.js",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.19.2",
"socket.io": "^4.7.5",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}This complete package.json defines the scripts: npm run dev for development with hot-reload. It lists stable, 2026-compatible versions. Copy-paste for instant setup and to avoid version conflicts.
Understanding the Digital Twin Flow
The physical twin is simulated by a loop generating random temperature data (18-35°C). The digital twin receives it via Socket.io, stores it, and visualizes it. Analogy: like a mirror reflecting your movements live. The REST API allows queries (GET /status), while WebSockets push updates every 2s for smooth real-time.
Main Server with Simulation
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: { origin: "*" }
});
app.use(cors());
app.use(express.static('public'));
// État du Digital Twin
let twinState = {
temperature: 20,
humidity: 50,
status: 'nominal',
timestamp: new Date().toISOString()
};
// Simulation physique (loop toutes les 2s)
setInterval(() => {
twinState.temperature = Math.max(18, Math.min(35, twinState.temperature + (Math.random() - 0.5) * 2));
twinState.humidity = Math.max(30, Math.min(80, twinState.humidity + (Math.random() - 0.5) * 5));
twinState.status = twinState.temperature > 30 ? 'alerte' : 'nominal';
twinState.timestamp = new Date().toISOString();
io.emit('update', twinState); // Broadcast au twin digital
}, 2000);
// API REST
app.get('/api/status', (req, res) => {
res.json(twinState);
});
io.on('connection', (socket) => {
console.log('Client connecté');
socket.emit('update', twinState); // Sync initial
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});This Express+Socket.io server simulates the 'physical' side via setInterval: temperature fluctuates realistically (±1°C random). Broadcast via io.emit updates all clients. GET /api/status provides a synchronous snapshot. Pitfall: without CORS, the frontend blocks; here configured for * in dev.
Client HTML Interface
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Twin - Capteur Température</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
.gauge { width: 200px; height: 100px; border: 5px solid #ddd; border-radius: 50px; margin: 20px 0; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.alert { background: #ff6b6b; color: white; }
#log { height: 200px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<h1>Digital Twin : Capteur IoT</h1>
<div>Temperature: <div id="temp" class="gauge">20°C</div></div>
<div>Humidité: <span id="humidity">50%</span></div>
<div>Statut: <span id="status">nominal</span></div>
<div>Dernier update: <span id="timestamp"></span></div>
<button onclick="fetchStatus()">Refresh API</button>
<div id="log"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('update', (data) => {
document.getElementById('temp').textContent = data.temperature.toFixed(1) + '°C';
document.getElementById('humidity').textContent = data.humidity + '%';
document.getElementById('status').textContent = data.status;
document.getElementById('timestamp').textContent = new Date(data.timestamp).toLocaleString('fr-FR');
document.getElementById('temp').className = data.status === 'alerte' ? 'gauge alert' : 'gauge';
log('Update reçu: ' + JSON.stringify(data));
});
function fetchStatus() {
fetch('/api/status').then(res => res.json()).then(data => {
socket.emit('update', data); // Trigger visu
log('API fetch: ' + JSON.stringify(data));
});
}
function log(msg) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>' + new Date().toLocaleTimeString() + ': ' + msg + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
</body>
</html>This standalone HTML integrates Socket.io client-side for live updates. Visual CSS gauges change color on alert (>30°C). Button tests the REST API. Log acts like a console for debugging. Create the 'public/' folder first; otherwise, static files return 404.
Running and Testing
Start with npm run dev. Open http://localhost:3000. Watch auto-updates every 2s, test the Refresh button. The 'physical' twin (server) generates data, the 'digital' twin (browser) mirrors it perfectly. Analogy: a virtual coach tracking your runs live.
Adding Bidirectional Control
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: { origin: "*" }
});
app.use(cors());
app.use(express.static('public'));
app.use(express.json());
let twinState = {
temperature: 20,
humidity: 50,
setpoint: 22,
status: 'nominal',
timestamp: new Date().toISOString()
};
setInterval(() => {
// Simulation influencée par setpoint
const delta = twinState.setpoint - twinState.temperature;
twinState.temperature += (Math.random() - 0.5) + (delta > 0 ? 0.1 : -0.1);
twinState.temperature = Math.max(18, Math.min(35, twinState.temperature));
twinState.humidity = Math.max(30, Math.min(80, twinState.humidity + (Math.random() - 0.5) * 3));
twinState.status = twinState.temperature > 30 ? 'alerte' : 'nominal';
twinState.timestamp = new Date().toISOString();
io.emit('update', twinState);
}, 2000);
app.get('/api/status', (req, res) => res.json(twinState));
app.post('/api/setpoint', (req, res) => {
twinState.setpoint = Math.max(18, Math.min(28, parseFloat(req.body.setpoint)));
res.json({ success: true, setpoint: twinState.setpoint });
});
io.on('connection', (socket) => {
socket.emit('update', twinState);
socket.on('setSetpoint', (setpoint) => {
twinState.setpoint = Math.max(18, Math.min(28, setpoint));
socket.emit('update', twinState);
});
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});Upgrade: adds POST /api/setpoint and 'setSetpoint' socket for twin control (setpoint influences simulation). The loop reacts to setpoint like a real thermostat. Secured with 18-28°C limits. Replace server.js for full bidirectionality.
Interactive Client with Controls
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Twin - Capteur Température</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
.gauge { width: 200px; height: 100px; border: 5px solid #ddd; border-radius: 50px; margin: 20px 0; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.alert { background: #ff6b6b; color: white; }
#log { height: 200px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
input { padding: 10px; font-size: 16px; }
</style>
</head>
<body>
<h1>Digital Twin : Capteur IoT Interactif</h1>
<div>Temperature: <div id="temp" class="gauge">20°C</div></div>
<div>Humidité: <span id="humidity">50%</span></div>
<div>Setpoint: <span id="setpoint">22°C</span></div>
<div>Statut: <span id="status">nominal</span></div>
<div>Dernier update: <span id="timestamp"></span></div>
<input type="number" id="newSetpoint" min="18" max="28" value="22" placeholder="Nouveau setpoint">
<button onclick="setSetpoint()">Appliquer Setpoint</button>
<button onclick="fetchStatus()">Refresh API</button>
<div id="log"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('update', (data) => {
document.getElementById('temp').textContent = data.temperature.toFixed(1) + '°C';
document.getElementById('humidity').textContent = data.humidity + '%';
document.getElementById('setpoint').textContent = data.setpoint + '°C';
document.getElementById('status').textContent = data.status;
document.getElementById('timestamp').textContent = new Date(data.timestamp).toLocaleString('fr-FR');
document.getElementById('temp').className = data.status === 'alerte' ? 'gauge alert' : 'gauge';
log('Update: ' + JSON.stringify(data));
});
function setSetpoint() {
const setpoint = parseFloat(document.getElementById('newSetpoint').value);
socket.emit('setSetpoint', setpoint);
log('Setpoint envoyé: ' + setpoint);
}
function fetchStatus() {
fetch('/api/status').then(res => res.json()).then(data => {
log('API: ' + JSON.stringify(data));
});
}
function log(msg) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>' + new Date().toLocaleTimeString() + ': ' + msg + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
</body>
</html>Adds setpoint input + button for Socket.io control. The twin reacts: temperature converges toward setpoint. Tests POST indirectly. Replace index.html; enhanced UI now shows setpoint.
Best Practices
- Secure data: Add Zod/Joi validation on API for realistic bounds.
- Scale with DB: Replace in-memory state with Redis/MongoDB for persistence.
- Monitoring: Integrate Prometheus for twin metrics (update latency).
- Production security: HTTPS + JWT auth on sockets; restrict CORS to known domains.
- Testing: Add Jest to simulate loops and connections.
Common Errors to Avoid
- No CORS: Frontend blocks; always configure cors() or io.cors.
- Memory leaks: setInterval without clearInterval leaks; use ref variables.
- No validation: Extreme setpoints crash sim; clamp with Math.min/max.
- Missing initial sync: New clients join without state; emit('update') on connection.
Next Steps
Master advanced Digital Twins with Unity for 3D or AWS IoT TwinMaker. Check out our Node.js IoT course. Resources: Socket.io Docs, Gartner Digital Twins. Next: integrate ML for predictions!