TCP Servers (net Module)
The net module is Node's API for TCP — the reliable, ordered, connection-oriented transport that HTTP, databases, and most application protocols ride on. With it you create TCP servers and clients, and each connection is a socket: a duplex stream of bytes. Master net and you understand the layer beneath every web server you'll ever write.
A TCP server
import { createServer } from 'node:net'
const server = createServer((socket) => {
// Called once per client connection. `socket` is a Duplex stream.
console.log('client connected:', socket.remoteAddress, socket.remotePort)
socket.write('Hello from the server\n')
socket.on('data', (chunk) => console.log('received:', chunk.toString()))
socket.on('end', () => console.log('client disconnected'))
socket.on('error', (err) => console.error('socket error:', err.message))
})
server.listen(4000, () => console.log('listening on :4000'))A TCP client
import { connect } from 'node:net'
const socket = connect({ port: 4000, host: '127.0.0.1' }, () => {
console.log('connected to server')
socket.write('ping')
})
socket.on('data', (chunk) => {
console.log('server says:', chunk.toString())
socket.end() // close our side after one reply
})
socket.on('error', (err) => console.error(err.message))The crucial gotcha: TCP has no message boundaries
The fix: message framing
To recover discrete messages you must frame them. The two standard approaches are a delimiter (e.g. newline-terminated) or a length prefix (send the byte count first). Here's newline-delimited framing — buffer across data events and split on the delimiter:
Newline-delimited message framing
import { createServer } from 'node:net'
createServer((socket) => {
let buffer = ''
socket.on('data', (chunk) => {
buffer += chunk.toString()
let index
// Pull out every COMPLETE line; keep the trailing partial in buffer
while ((index = buffer.indexOf('\n')) !== -1) {
const message = buffer.slice(0, index)
buffer = buffer.slice(index + 1)
handleMessage(socket, message)
}
})
}).listen(4000)
function handleMessage(socket, message) {
socket.write(`echo: ${message}\n`)
}Sockets are streams — use them as such
import { createServer } from 'node:net'
// Echo server via piping (socket is duplex: pipe readable→writable side):
createServer((socket) => socket.pipe(socket)).listen(4000)
// Or consume with async iteration + backpressure:
createServer(async (socket) => {
for await (const chunk of socket) {
socket.write(chunk) // respects backpressure naturally
}
}).listen(4001)Useful socket properties and methods
Member | Purpose |
|---|---|
| Who connected |
| Send bytes (returns |
| Send optional final data, then close our side |
| Abruptly tear down (on error/abuse) |
| Emit |
| Detect dead peers on idle connections |
| Disable Nagle — send small packets immediately |
Handling errors and cleanup
Server lifecycle
const server = createServer(/* ... */)
server.listen(4000)
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') console.error('port 4000 already in use')
})
// Graceful shutdown: stop accepting, let existing connections finish
process.on('SIGINT', () => server.close(() => process.exit(0)))