How to Use the Web Serial API in JavaScript

This article provides a practical guide to the Web Serial API, explaining how web applications can directly communicate with hardware devices over serial connections. You will learn the core concepts of the API, security and permission models, and step-by-step implementations for requesting device access, reading incoming serial data streams, and sending data to connected microcontrollers or peripherals using JavaScript.

What is the Web Serial API?

The Web Serial API provides a direct communication bridge between web pages and serial hardware devices, such as microcontrollers (e.g., Arduino, ESP32), 3D printers, barcode scanners, and custom sensors. Historically, interacting with local serial ports required native desktop software, specialized browser plugins, or local proxy servers. The Web Serial API removes these intermediaries by exposing serial ports directly to the browser via JavaScript.

Security and Browser Requirements

Because direct hardware access carries inherent security risks, the Web Serial API implements strict security guardrails:

Requesting and Opening a Port

To initiate a connection, call navigator.serial.requestPort(). This prompts the user with a browser dialog listing available serial devices. Once the user selects a device, the port must be opened with a specified baud rate.

async function connectSerial() {
  try {
    // Prompt user to select a serial port
    const port = await navigator.serial.requestPort();

    // Open the serial port with a specific configuration
    await port.open({ baudRate: 9600 });
    
    console.log("Connected to serial device.");
    return port;
  } catch (error) {
    console.error("Connection failed:", error);
  }
}

You can optionally pass filter parameters to requestPort() to only display devices with specific USB Vendor IDs (VID) or Product IDs (PID).

Reading Data from a Serial Port

The Web Serial API uses the Streams API to handle continuous data flow. Reading data involves getting a reader from port.readable and listening for incoming chunks in an asynchronous loop.

To handle text data, pipe the stream through a TextDecoderStream.

async function readSerialData(port) {
  const textDecoder = new TextDecoderStream();
  const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
  const reader = textDecoder.readable.getReader();

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        // Stream has been closed
        break;
      }
      if (value) {
        console.log("Received data:", value);
      }
    }
  } catch (error) {
    console.error("Read error:", error);
  } finally {
    reader.releaseLock();
  }
}

Writing Data to a Serial Port

Sending data uses the port.writable stream. You can write raw binary data using Uint8Array or pipe text through a TextEncoderStream to transmit string data directly.

async function writeSerialData(port, message) {
  const textEncoder = new TextEncoderStream();
  const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
  const writer = textEncoder.writable.getWriter();

  try {
    await writer.write(message + "\n");
    console.log("Data sent:", message);
  } catch (error) {
    console.error("Write error:", error);
  } finally {
    // Release the writer lock so the port can be reused or closed
    writer.releaseLock();
  }
}

Closing the Serial Port

To cleanly disconnect from a device, all active stream readers and writers must release their locks before calling port.close().

async function disconnectSerial(port, reader, writer) {
  if (reader) {
    await reader.cancel();
  }
  if (writer) {
    await writer.close();
  }
  await port.close();
  console.log("Serial port closed.");
}

By leveraging standard stream piping and promise-based interfaces, the Web Serial API enables developers to build real-time hardware dashboards, configuration tools, and device interfaces entirely within the browser.