| |

Node.js 18 🟢 Binary Data Manipulation with Buffers and TypedArrays

JavaScript was designed for text. Its strings are sequences of UTF-16 code units, its numbers are 64-bit floats, and its arrays are flexible collections that can hold anything. None of these types is suitable for binary data. When Node.js needs to read a file, receive a network packet, or generate a cryptographic hash, it works with raw bytes. The Buffer class and the TypedArray family exist to represent those bytes. A Buffer is Node.js’s original binary type, a subclass of Uint8Array that adds Node-specific methods. A TypedArray is the ECMAScript standard for typed binary views: Uint8Array, Int32Array, Float64Array, and others, each representing a fixed-width numeric type.

The two are closely related. Buffer is a Uint8Array with extra methods, and the same underlying memory can be viewed through multiple typed arrays with different element sizes. This aliasing is what makes binary manipulation efficient: a Uint8Array of 4 bytes can be read as an Int32Array of 1 element, or as a Float32Array of 1 element, without copying. The ArrayBuffer is the raw memory block, and the typed arrays are views into it. The DataView is a third view that allows reading and writing multi-byte values at arbitrary offsets with explicit endianness.

This chapter covers the Buffer class, the TypedArray family, the ArrayBuffer and DataView, the encoding and decoding functions, the conversion between these types, the endianness rules, and the patterns that make binary manipulation safe and readable.

Key point: A Buffer is a Uint8Array with Node-specific methods. All typed arrays are views into an underlying ArrayBuffer, and multiple views can share the same memory. Buffer handles text encodings (utf8, hex, base64, and others). DataView handles multi-byte numeric values with explicit endianness. Use Buffer.from to create a buffer from a string, array, or another buffer, and use the typed array constructors to create numeric views.


Why Buffer and TypedArray exist

The text-only problem. JavaScript strings cannot represent arbitrary byte sequences. A byte like 0xFF is not a valid UTF-8 sequence on its own, and the String type would convert it to a replacement character. Binary data — file contents, network packets, cryptographic digests — must be represented as bytes, not as text. The Buffer and Uint8Array types are the representation.

The performance problem. A regular JavaScript array of numbers is a flexible object with per-element overhead. Storing a megabyte of binary data as [0, 1, 2, ...] uses many times the memory and is far slower than storing it in a contiguous typed array. Typed arrays are backed by a single ArrayBuffer, and their elements are stored contiguously with no per-element overhead.

The interoperability problem. Node.js APIs return Buffer objects. Web APIs return ArrayBuffer objects. A code path that goes from a Node.js file read to a Web Crypto operation must convert between them. Buffer is a Uint8Array, so it can be passed to any API that expects a Uint8Array. The ArrayBuffer can be extracted from a Buffer with .buffer, though the view may not cover the whole buffer.

The endianness problem. A 32-bit integer can be stored in memory with the most significant byte first (big-endian) or last (little-endian). Network protocols use big-endian; most CPUs use little-endian. The Buffer methods have explicit endianness (readUInt32BE versus readUInt32LE), and the DataView requires the endianness as a parameter. The explicit choice prevents the silent bugs that occur when the platform’s byte order is assumed.

The view problem. The same bytes can be interpreted as different numeric types depending on the view. A four-byte sequence can be a Uint8Array of four elements, a Uint16Array of two elements, an Int32Array of one element, or a Float32Array of one element. The ArrayBuffer is the shared memory, and each typed array is a view. This makes binary parsing efficient: the same buffer can be read with different views without copying.


a. Creating a Buffer

The Buffer class has several factory methods. The Buffer constructor itself is deprecated; use the static methods instead.

From a string:

const buf = Buffer.from('hello', 'utf8');
console.log(buf);           // <Buffer 68 65 6c 6c 6f>
console.log(buf.length);    // 5

The second argument is the encoding. The default is 'utf8'. Other encodings include 'hex', 'base64', 'latin1', 'ascii', 'utf16le', and 'base64url' .

From an array of bytes:

const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);

Each element must be an integer between 0 and 255. Values outside the range are truncated modulo 256.

From an ArrayBuffer:

const ab = new ArrayBuffer(5);
const buf = Buffer.from(ab);

From an existing Buffer:

const buf2 = Buffer.from(buf);  // copies the data

Buffer.from(buffer) copies the bytes. To share the memory, use Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).

Allocating uninitialized memory:

const buf = Buffer.alloc(10);        // filled with zeros
const buf2 = Buffer.allocUnsafe(10); // not initialized

Buffer.alloc zeros the memory. Buffer.allocUnsafe does not, which is faster but can expose old data if the buffer is read before it is written. Use allocUnsafe only when the buffer will be fully overwritten before use .

From a size:

const buf = Buffer.alloc(1024);

b. Reading and writing Buffer data

The Buffer class provides read and write methods for integers, floats, and strings. The integer methods have explicit endianness.

Integers:

const buf = Buffer.alloc(8);

buf.writeUInt8(0x48, 0);
buf.writeUInt16BE(0x1234, 1);
buf.writeUInt32LE(0xdeadbeef, 3);

console.log(buf.readUInt8(0));       // 0x48
console.log(buf.readUInt16BE(1));    // 0x1234
console.log(buf.readUInt32LE(3));    // 0xdeadbeef

The BE and LE suffixes mean big-endian and little-endian. The numeric argument is the offset in bytes.

Floats:

const buf = Buffer.alloc(8);

buf.writeFloatBE(3.14, 0);
buf.writeDoubleLE(2.71828, 4);

console.log(buf.readFloatBE(0));     // 3.14
console.log(buf.readDoubleLE(4));    // 2.71828

Strings:

const buf = Buffer.alloc(20);

buf.write('hello', 0, 'utf8');
buf.write('world', 6, 'utf8');

console.log(buf.toString('utf8', 0, 5));   // 'hello'
console.log(buf.toString('utf8', 6, 11));  // 'world'

The toString method takes the encoding and optional start and end offsets.

BigInt:

const buf = Buffer.alloc(8);
buf.writeBigUInt64BE(12345678901234567890n, 0);
console.log(buf.readBigUInt64BE(0));

The BigInt methods handle 64-bit integers that exceed the safe range for JavaScript numbers.


c. The TypedArray family

Buffer is a Uint8Array. The TypedArray family includes:

TypeElement sizeRange
Uint8Array1 byte0 to 255
Int8Array1 byte-128 to 127
Uint16Array2 bytes0 to 65535
Int16Array2 bytes-32768 to 32767
Uint32Array4 bytes0 to 4294967295
Int32Array4 bytes-2147483648 to 2147483647
Float32Array4 bytes32-bit float
Float64Array8 bytes64-bit float
BigInt64Array8 bytes64-bit signed BigInt
BigUint64Array8 bytes64-bit unsigned BigInt

Each type has a constructor that creates a new array or a view into an existing ArrayBuffer.

const u8 = new Uint8Array(4);          // 4 bytes, zeroed
const i32 = new Int32Array(2);         // 8 bytes, zeroed
const f64 = new Float64Array(1);       // 8 bytes, zeroed

The length property is the number of elements, and the byteLength property is the number of bytes.

const u32 = new Uint32Array(4);
console.log(u32.length);      // 4
console.log(u32.byteLength);  // 16

Typed arrays have methods for iteration, mapping, and transformation, similar to regular arrays but optimized for numeric data.


d. ArrayBuffer and views

An ArrayBuffer is a fixed-length block of raw memory. It has no type and no methods for reading or writing; it is just a buffer.

const ab = new ArrayBuffer(16);
console.log(ab.byteLength);  // 16

A typed array is a view into an ArrayBuffer. Multiple views can share the same buffer, which is how the same bytes can be read as different types.

const ab = new ArrayBuffer(4);
const u8 = new Uint8Array(ab);
const u16 = new Uint16Array(ab);
const u32 = new Uint32Array(ab);

u8[0] = 0x01;
u8[1] = 0x02;
u8[2] = 0x03;
u8[3] = 0x04;

console.log(u16[0]);  // 0x0201 or 0x0102, depending on endianness
console.log(u32[0]);  // 0x04030201 or 0x01020304

The u16 view reads two bytes as one 16-bit value, and the u32 view reads four bytes as one 32-bit value. The values depend on the platform’s byte order.

A view can be offset and length-limited:

const ab = new ArrayBuffer(16);
const view = new Uint8Array(ab, 4, 8);  // offset 4, length 8
console.log(view.byteOffset);  // 4
console.log(view.length);      // 8

The byteOffset is the index in the buffer where the view starts, and the length is the number of elements.

A Buffer can be created over an ArrayBuffer without copying:

const ab = new ArrayBuffer(8);
const buf = Buffer.from(ab);

The Buffer.from(ab) creates a view that shares the memory. Modifications through the buffer are visible through any typed array over the same buffer .


e. DataView for explicit endianness

The DataView is a view that provides methods for reading and writing multi-byte values with explicit endianness. It does not have the aliasing behavior of typed arrays; it reads and writes at a specified byte offset.

const ab = new ArrayBuffer(8);
const view = new DataView(ab);

view.setUint16(0, 0x1234, false);  // big-endian
view.setUint32(2, 0xdeadbeef, true); // little-endian

console.log(view.getUint16(0, false));  // 0x1234
console.log(view.getUint32(2, true));   // 0xdeadbeef

The third argument is the littleEndian boolean. false means big-endian, and true means little-endian. This explicit choice is the advantage of DataView over typed arrays: the byte order is part of the call, not the platform.

The DataView methods include getInt8, getUint8, getInt16, getUint16, getInt32, getUint32, getFloat32, getFloat64, getBigInt64, and getBigUint64, each with a corresponding set method.

DataView is the right tool for parsing binary formats that specify the byte order, such as network protocols, file headers, and serialization formats.


f. Encoding and decoding

The Buffer class handles text encodings. The toString method decodes a buffer to a string, and the Buffer.from method encodes a string to a buffer.

UTF-8:

const buf = Buffer.from('hello', 'utf8');
console.log(buf.toString('utf8'));  // 'hello'

Hex:

const buf = Buffer.from('deadbeef', 'hex');
console.log(buf);                    // <Buffer de ad be ef>
console.log(buf.toString('hex'));    // 'deadbeef'

The hex encoding is commonly used for cryptographic hashes, identifiers, and human-readable binary data. Each byte is two hex characters.

Base64:

const buf = Buffer.from('hello', 'utf8');
console.log(buf.toString('base64'));  // 'aGVsbG8='

const decoded = Buffer.from('aGVsbG8=', 'base64');
console.log(decoded.toString('utf8'));  // 'hello'

Base64 is used for encoding binary data in text contexts, such as JSON payloads and data URLs. The base64url variant uses URL-safe characters.

Latin1:

const buf = Buffer.from([0xe9, 0xe8]);
console.log(buf.toString('latin1'));  // 'éè'

Latin1 (also called ISO-8859-1) maps each byte to a character in the range U+0000 to U+00FF.

The TextEncoder and TextDecoder classes are the Web standard alternatives:

const encoder = new TextEncoder();
const buf = encoder.encode('hello');  // Uint8Array

const decoder = new TextDecoder('utf8');
console.log(decoder.decode(buf));  // 'hello'

The TextEncoder produces a Uint8Array, and the TextDecoder accepts an ArrayBufferView. These are portable across Node.js and browsers.


Complete Example Session

// ============================================
// PART 1: CREATE A BUFFER FROM A STRING
// ============================================
const buf = Buffer.from('hello', 'utf8');
console.log(buf);           // <Buffer 68 65 6c 6c 6f>
console.log(buf.length);    // 5
// ============================================
// PART 2: CREATE A BUFFER FROM BYTES
// ============================================
const bytes = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
console.log(bytes.toString('utf8'));  // 'Hello'
// ============================================
// PART 3: ALLOCATE A BUFFER
// ============================================
const buf = Buffer.alloc(10);
console.log(buf);  // <Buffer 00 00 00 00 00 00 00 00 00 00>

const unsafe = Buffer.allocUnsafe(10);
console.log(unsafe.length);  // 10
// ============================================
// PART 4: WRITE AND READ INTEGERS
// ============================================
const buf = Buffer.alloc(8);

buf.writeUInt8(0x48, 0);
buf.writeUInt16BE(0x1234, 1);
buf.writeUInt32LE(0xdeadbeef, 3);

console.log(buf.readUInt8(0));       // 0x48
console.log(buf.readUInt16BE(1));    // 0x1234
console.log(buf.readUInt32LE(3));    // 0xdeadbeef
// ============================================
// PART 5: WRITE AND READ FLOATS
// ============================================
const buf = Buffer.alloc(8);
buf.writeFloatBE(3.14, 0);
buf.writeDoubleLE(2.71828, 4);

console.log(buf.readFloatBE(0));    // 3.14
console.log(buf.readDoubleLE(4));   // 2.71828
// ============================================
// PART 6: TYPED ARRAYS
// ============================================
const u8 = new Uint8Array(4);
const i32 = new Int32Array(2);
const f64 = new Float64Array(1);

console.log(u8.length);       // 4
console.log(i32.byteLength);  // 8
console.log(f64.byteLength);  // 8
// ============================================
// PART 7: SHARED ARRAYBUFFER
// ============================================
const ab = new ArrayBuffer(4);
const u8 = new Uint8Array(ab);
const u32 = new Uint32Array(ab);

u8[0] = 0x01;
u8[1] = 0x02;
u8[2] = 0x03;
u8[3] = 0x04;

console.log(u32[0]);  // platform-dependent
// ============================================
// PART 8: DATAVIEW WITH EXPLICIT ENDIANNESS
// ============================================
const ab = new ArrayBuffer(8);
const view = new DataView(ab);

view.setUint16(0, 0x1234, false);   // big-endian
view.setUint32(2, 0xdeadbeef, true); // little-endian

console.log(view.getUint16(0, false));  // 0x1234
console.log(view.getUint32(2, true));   // 0xdeadbeef
// ============================================
// PART 9: HEX AND BASE64
// ============================================
const buf = Buffer.from('deadbeef', 'hex');
console.log(buf);                    // <Buffer de ad be ef>
console.log(buf.toString('hex'));    // 'deadbeef'

const b64 = Buffer.from('hello', 'utf8').toString('base64');
console.log(b64);                    // 'aGVsbG8='
console.log(Buffer.from(b64, 'base64').toString('utf8'));  // 'hello'
// ============================================
// PART 10: TEXTENCODER AND TEXTDECODER
// ============================================
const encoder = new TextEncoder();
const decoder = new TextDecoder('utf8');

const encoded = encoder.encode('hello');
console.log(encoded);  // Uint8Array(5) [104, 101, 108, 108, 111]

console.log(decoder.decode(encoded));  // 'hello'

These ten parts cover creating a buffer from a string, creating a buffer from bytes, allocating a buffer, reading and writing integers, reading and writing floats, typed arrays, shared ArrayBuffer, DataView with explicit endianness, hex and base64 encoding, and TextEncoder and TextDecoder.


Quick Reference

Buffer Creation

MethodPurpose
Buffer.from(string, encoding)From a string
Buffer.from(array)From byte values
Buffer.from(arrayBuffer)View over an ArrayBuffer
Buffer.from(buffer)Copy of a buffer
Buffer.alloc(n)Zeroed buffer of size n
Buffer.allocUnsafe(n)Uninitialized buffer

Buffer Read/Write

MethodType
readUInt8 / writeUInt88-bit unsigned
readInt16BE / writeInt16BE16-bit signed big-endian
readUInt32LE / writeUInt32LE32-bit unsigned little-endian
readFloatBE / writeFloatBE32-bit float
readDoubleLE / writeDoubleLE64-bit float
readBigUInt64BE64-bit unsigned BigInt
toString(encoding)Decode to string

Typed Arrays

TypeBytes per Element
Uint8Array1
Int8Array1
Uint16Array2
Int16Array2
Uint32Array4
Int32Array4
Float32Array4
Float64Array8
BigInt64Array8

Encodings

EncodingPurpose
utf8Text (default)
hexTwo chars per byte
base64Text-safe binary
base64urlURL-safe base64
latin1Byte-to-char mapping
utf16leUTF-16 little-endian
ascii7-bit ASCII

DataView Methods

MethodType
getUint8 / setUint88-bit unsigned
getInt16 / setInt1616-bit signed
getUint32 / setUint3232-bit unsigned
getFloat32 / setFloat3232-bit float
getFloat64 / setFloat6464-bit float
getBigUint64 / setBigUint6464-bit BigInt

Best Practices

✅ Do This:

// Use Buffer.from for creation
const buf = Buffer.from('hello', 'utf8');

// Use Buffer.alloc for zeroed memory
const safe = Buffer.alloc(10);

// Use explicit endianness
buf.writeUInt32BE(value, 0);
buf.writeUInt32LE(value, 0);

// Use DataView for multi-byte parsing
const view = new DataView(ab);
view.getUint16(0, false);

// Use hex for hashes and identifiers
const hex = buf.toString('hex');

// Use TextEncoder for portable code
const encoded = new TextEncoder().encode('hello');

❌ Don’t Do This:

// Use the deprecated Buffer constructor
const buf = new Buffer(10);  // ❌ use Buffer.alloc

// Use allocUnsafe without writing first
const buf = Buffer.allocUnsafe(10);  // ❌ may expose old data

// Assume endianness
buf.writeUInt32(value, 0);  // ❌ does not exist

// Mix up byte and element offsets
const u32 = new Uint32Array(ab, 2);  // ❌ offset must be a multiple of 4

// Concatenate strings for binary data
const data = '\xff\xfe';  // ❌ use a Buffer

// Use latin1 for arbitrary binary
const text = buf.toString('latin1');  // ⚠️ may lose data

Common Pitfalls

PitfallWhy It HappensFix
allocUnsafe exposes dataBuffer not written before readUse alloc or write first
Endianness mismatchAssumed the wrong byte orderUse BE/LE explicitly
Alignment errorTyped array offset not a multipleAlign the offset
RangeError on readOffset beyond the bufferCheck byteLength
Unicode string lengthUTF-16 length versus byte lengthUse Buffer.byteLength
Incorrect base64Used the wrong variantUse base64url for URLs
Shared memory surpriseViews share the same bufferCopy if independence is needed

Real-World Examples

1. Read a File

const data = await readFile('image.png');  // Buffer

2. Hex Hash

const hash = createHash('sha256').update('data').digest('hex');

3. Base64 Image

const b64 = data.toString('base64');
const url = `data:image/png;base64,${b64}`;

4. Parse a Header

const view = new DataView(buffer);
const version = view.getUint8(0);
const length = view.getUint16(1, false);

5. Write a Protocol Message

const buf = Buffer.alloc(8);
buf.writeUInt32BE(0xdeadbeef, 0);
buf.writeUInt32LE(0x12345678, 4);

6. Convert to Uint8Array

const u8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);

7. Concatenate Buffers

const combined = Buffer.concat([buf1, buf2]);

8. Compare Buffers

if (buf1.equals(buf2)) { }

9. Slice a Buffer

const slice = buf.subarray(0, 5);

10. Generate Random Bytes

const random = randomBytes(16);

Visual

Buffer, ArrayBuffer, and Views

┌──────────────────────────────────────────────────────────────┐
│  ArrayBuffer (raw memory)                                    │
│  ┌────┬────┬────┬────┐                                       │
│  │ 01 │ 02 │ 03 │ 04 │                                       │
│  └────┴────┴────┴────┘                                       │
│    ▲         ▲                                               │
│    │         │                                               │
│  Uint8Array  Uint32Array                                     │
│  └── 4 elements, 4 bytes                                     │
│              └── 1 element, 4 bytes                          │
│                                                              │
│  Multiple views share the same memory.                       │
└──────────────────────────────────────────────────────────────┘

Buffer Encoding Flow

┌──────────────────────────────────────────────────────────────┐
│  String → Buffer:                                            │
│  Buffer.from('hello', 'utf8')                                │
│  └── <Buffer 68 65 6c 6c 6f>                                 │
│                                                              │
│  Buffer → String:                                            │
│  buf.toString('hex')                                         │
│  └── '68656c6c6f'                                            │
│                                                              │
│  Buffer → Base64:                                            │
│  buf.toString('base64')                                      │
│  └── 'aGVsbG8='                                              │
└──────────────────────────────────────────────────────────────┘

Endianness

┌──────────────────────────────────────────────────────────────┐
│  VALUE: 0x12345678                                           │
│                                                              │
│  BIG-ENDIAN (network byte order):                            │
│  ┌────┬────┬────┬────┐                                       │
│  │ 12 │ 34 │ 56 │ 78 │                                       │
│  └────┴────┴────┴────┘                                       │
│                                                              │
│  LITTLE-ENDIAN (x86, ARM):                                   │
│  ┌────┬────┬────┬────┐                                       │
│  │ 78 │ 56 │ 34 │ 12 │                                       │
│  └────┴────┴────┴────┘                                       │
│                                                              │
│  Use the BE/LE suffix explicitly.                            │
└──────────────────────────────────────────────────────────────┘

DataView vs TypedArray

┌──────────────────────────────────────────────────────────────┐
│  TYPED ARRAY:                                                │
│  u32[0] = 0x12345678;                                        │
│  └── Uses platform endianness                                │
│  └── Offset must be aligned to element size                  │
│                                                              │
│  DATAVIEW:                                                   │
│  view.setUint32(0, 0x12345678, false);  // big-endian        │
│  view.setUint32(0, 0x12345678, true);   // little-endian     │
│  └── Explicit endianness                                     │
│  └── Any offset                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
BufferNode.js binary type, subclass of Uint8Array
ArrayBufferRaw fixed-length memory block
TypedArrayTyped view into an ArrayBuffer
DataViewView with explicit endianness
CreationBuffer.from, Buffer.alloc, Buffer.allocUnsafe
Read/writereadUInt8, writeUInt32BE, readDoubleLE
EndiannessBE (big) and LE (little)
Encodingsutf8, hex, base64, base64url, latin1
AlignmentTyped array offset must be a multiple of element size
TextEncoderPortable encode to Uint8Array
TextDecoderPortable decode from a typed array

Key takeaways:

  • Buffer is a Uint8Array with Node-specific methods. It is the original binary type in Node.js, and it is what every I/O API returns and accepts.
  • All typed arrays are views into an ArrayBuffer. Multiple views can share the same memory, which allows the same bytes to be read as different numeric types.
  • The DataView provides explicit endianness. Use it for parsing binary formats that specify the byte order, such as network protocols and file headers.
  • The BE and LE suffixes on the Buffer methods specify endianness. readUInt32BE reads big-endian, and readUInt32LE reads little-endian.
  • Buffer.from creates a buffer from a string, array, or another buffer. Buffer.alloc zeros the memory, and Buffer.allocUnsafe does not.
  • The Buffer class handles text encodings. utf8, hex, base64, base64url, and latin1 are the common ones.
  • TextEncoder and TextDecoder are the portable alternatives. They produce and accept typed arrays, so the same code works in Node.js and the browser.

Remember: JavaScript’s text-oriented types are not suitable for binary data. The Buffer and TypedArray family exists to represent raw bytes, and the ArrayBuffer is the shared memory that all views are built on. The Buffer class adds Node-specific read and write methods with explicit endianness. The DataView provides the same control at any offset. The encoding functions convert between bytes and text. Use the typed arrays when the data is numeric and the layout is uniform, use the DataView when the byte order must be explicit, and use the Buffer when the data is text or when the Node.js API requires it.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!