Stop paying the JSON tax

By Kent Wu

April 21, 2026

If you’re building data-intensive applications in Node.js, you’re hitting a ceiling you can’t quite name.

The tax you stopped noticing

Your database stores your data with types, structure, and schema. Then you query it, and somewhere between the engine and your await db.query(...), the whole thing gets shredded. Types become strings. Structure becomes flat objects. Schema becomes unknown. You spend the next twenty lines putting it all back together—parse, validate, transform, coerce—every query, every time.

This is the JSON tax. You’ve been paying it so long it feels like a law of nature. It isn’t. It’s a design choice, and it compounds across every driver in your stack.

The N×MN \times M problem

Open the package.json of any data-intensive Node.js application and count the database drivers. pg. mysql2. snowflake-sdk. @databricks/sql. @duckdb/node-api or @duckdb. Maybe better-sqlite3 for good measure. Each one is its own world: its own connection semantics, its own result format, its own type story, its own binary dependencies.

In the application layer, we solved this problem years ago. We compose React components. We compose middleware. We compose types. The entire TypeScript ecosystem is built on the premise that the right abstraction makes composition effortless.

But the data layer? The data layer stayed bespoke. Five drivers, five mental models, five entries in your dependency tree doing roughly the same thing in five incompatible ways.

Think about what the Language Server Protocol (LSP) did for editors. Before LSP, VS Code had to “learn” Ruby; Vim had to learn Go; and Sublime had to learn Rust—independently. If there were NN editors and MM languages, you needed N×MN \times M integrations to make the ecosystem work. LSP collapsed that into N+MN + M. It moved the intelligence into the language server and the interface into a single protocol. Any editor can now talk to any language.

The application layer got composable. The data layer never did.

This collapse requires two things: a standard format and a standard interface.

The format: Apache Arrow

Apache Arrow is the standard columnar memory format for analytical data interchange, built by over 1,000 contributors under the Apache Software Foundation over the past decade. The engines you’re already using—DuckDB, Databricks, Snowflake, BigQuery—all support Arrow.

At its core, Arrow is a specification for how structured data is laid out in memory. Because the format is language-independent and the layout is fully specified, any Arrow library in any language can operate on the same memory directly—no serialization, no conversion. Zero-copy interchange.

Using Arrow end-to-end is an increasingly common architecture for performance-critical applications. Uber, CARTO, and Foursquare power high-performance geospatial visualizations via Deck.gl, and JPMorganChase drives sub-second responsiveness on global trading desks through Perspective.

But a common format is only half the story. Arrow standardized the representation. Not how you ask for it.

The interface: ADBC — Arrow Database Connectivity

If you’ve been in this industry long enough, you might hear “database connectivity standard” and think ODBC. That’s the right instinct—ODBC has been powering applications since 1992. But ODBC was designed for a row-oriented world. It standardized the interface and left the data format to low-level row buffers, which applications then convert to JSON.

In the decades since, data engines went columnar—and ODBC didn’t follow. For analytical workloads, the driver itself has become the bottleneck: every query result gets converted from the engine’s native columnar format into rows, only to be converted back again.

In 2022, a group of Apache Arrow developers decided that 30 years of ODBC was enough, and set out to build its spiritual successor: ADBC (Arrow Database Connectivity). In the years since, vendors and open source projects have jumped in—Snowflake, DuckDB, Databricks, pandas, and Polars among them, with Microsoft adopting ADBC in Power BI and dbt Labs in dbt Fusion.

ADBC carries forward what ODBC got right: a standard interface for database connectivity. But ADBC standardizes the format too. The interface is the driver API. The format is Apache Arrow. One interface. Any database. Arrow in, Arrow out.

Remember the JSON tax? Using Arrow and ADBC is what it looks like to stop paying it. A DECIMAL(38,10) stays a decimal, instead of silently rounding itself into an IEEE 754 float. A BIGINT arrives as an actual 64-bit integer, not a string you have to remember to parse. A TIMESTAMP WITH TIME ZONE carries its timezone across the wire. Binary blobs aren’t base64’d and decoded on arrival. Nested types, lists, structs, unions—they survive the trip with their shape intact. Arrow has real types, and they make it to your application in one piece.

Bringing ADBC to Node.js

Today, we’re announcing @apache-arrow/adbc-driver-manager—the ADBC driver manager for Node.js.

In ADBC, a driver manager is a library that loads ADBC drivers at runtime and exposes them through a single, uniform API. It’s the part that makes “one interface for any database” actually work inside your application. You install this one package, then install and load drivers for the databases you need.

Driver managers for ADBC were already available in C/C++, Go, Java, Rust, Python, and R. Node.js was a conspicuous gap—until the ADBC 23 release earlier this month. Contributed to apache/arrow-adbc, this new library is built with napi-rs and Rust under the hood. On the surface, it’s a clean, minimal JavaScript API: connect to any database, get structured, typed, columnar data back.

To get started, install the driver manager and an ADBC driver for your database. The easiest way to install drivers is with dbc, the package manager for ADBC drivers:

npm install @apache-arrow/adbc-driver-manager
dbc install snowflake # install the Snowflake ADBC driver

Then import the driver manager, point it to the driver by name, connect to your database, and run a query:


// One interface. Any ADBC driver. Arrow out.
const db = new AdbcDatabase({
  driver: 'snowflake',
  databaseOptions: { uri: process.env.DB_URI },
});

const conn = await db.connect();

// What comes back isn't JSON. It's an Arrow Table—
// columnar, typed, and zero-copy-ready.
const table = await conn.query(`
  SELECT region, SUM(amount) as revenue
  FROM orders GROUP BY region
`);

// The data stays in its native format.
// No .map(), no .parse(), no GC thrashing.
const regions = table.getChild('region');
const revenue = table.getChild('revenue');

console.log(table.toString());

Swap snowflake for postgresql, sqlite, duckdb, or a dozen others—the connection code doesn’t change. (Your SQL will still need to speak each database’s dialect; ADBC standardizes connectivity, not SQL itself.)

The convergence

Today, fetch is the universal primitive for network I/O in JavaScript runtimes. It doesn’t care if you’re talking to a REST API, a GraphQL endpoint, or a static file. The data layer never had an equivalent—a single, standard interface that doesn’t care if you’re talking to Postgres, Snowflake, SQLite, or DuckDB.

@apache-arrow/adbc-driver-manager is the beginning of one.