A small, framework-agnostic JavaScript/TypeScript client for the Apache Iceberg REST Catalog.
Tracks the OpenAPI spec at apache-iceberg-1.11.0-rc1. The exact tag is exported as ICEBERG_REST_SPEC_TAG and is the source of truth for the conformance tests in CI — see spec-pin.json.
This library provides JavaScript and TypeScript developers with a straightforward way to interact with Apache Iceberg REST Catalogs. It's designed as a thin HTTP wrapper that mirrors the official REST API, making it easy to manage namespaces and tables from any JS/TS environment.
- REST API wrapper: Provide a 1:1 mapping to the Iceberg REST Catalog API
- Type safety: Full TypeScript support with strongly-typed request/response models
- Minimal footprint: No engine-specific logic, no heavy dependencies
- Stability: Production-ready for catalog management operations
- Vendor-agnostic: Works with any Iceberg REST Catalog implementation
This library intentionally does not support:
- Data operations: Reading or writing table data (Parquet files, etc.)
- Query execution: Use dedicated query engines (Spark, Trino, DuckDB, etc.)
- Engine integration: No Spark, Flink, or other engine-specific code
- Advanced features: Branching, tagging, time travel queries beyond metadata
- Views or multi-table transactions
These boundaries keep the library focused and maintainable. For data operations, pair this library with a query engine that supports Iceberg.
- Generic: Works with any Iceberg REST Catalog implementation, not tied to any specific vendor
- Minimal: Thin HTTP wrapper over the official REST API, no engine-specific logic
- Type-safe: First-class TypeScript support with strongly-typed request/response models
- Fetch-based: Uses native
fetchAPI with support for custom implementations - Universal: Targets Node 22+ and modern browsers (ES2020)
- Catalog-only: Focused on catalog operations (no data reading/Parquet support in v1.0)
📚 Full API documentation: supabase.github.io/iceberg-js
npm install iceberg-jsThe 1.0 release aligns the client with the Apache Iceberg REST Catalog OpenAPI spec. The breaking changes are limited to the listing methods and the updateTable body shape.
| Before (0.x) | After (1.0) |
|---|---|
await catalog.listNamespaces() returns NamespaceIdentifier[] |
returns { namespaces: NamespaceIdentifier[]; nextPageToken? } |
await catalog.listNamespaces({ namespace: ['x'] }) (parent) |
await catalog.listNamespaces({ parent: { namespace: ['x'] } }) |
await catalog.listTables({ namespace: ['x'] }) returns TableIdentifier[] |
returns { identifiers: TableIdentifier[]; nextPageToken? } |
updateTable({ properties: { … } }) |
updateTable({ updates: [{ action: 'set-properties', updates: { … } }] }) |
catalogName builds the path manually as /v1/<name>/... |
warehouse (or catalogName as alias) goes through GET /v1/config and uses the server-returned prefix. Closes #32. |
| Supported Node 18+ | Now requires Node 22+ (Node 18 and 20 are EOL or near-EOL). Drop in #46. |
Non-breaking additions:
Idempotency-Keyis automatically emitted on every POST/DELETE mutation.loadTable(id, { ifNoneMatch })enables conditional GETs (returnsnullon 304).loadTable(id, { snapshots: 'all' | 'refs' })controls how many snapshots the server includes.loadTableResult(id, options?)returns the full spec-shapedLoadTableResultplus the responseETag.createTableResult(namespace, request)mirror — returns the full spec-shapedLoadTableResult(soaccessDelegation: ['vended-credentials']callers can read the server-vended credentials).registerTable(namespace, request)andrenameTable(request)are now exposed.updateNamespacePropertiesis now exposed.- The full
TableUpdateandTableRequirementdiscriminated unions are exported. - Network-level errors (DNS, TLS, offline) now surface as
IcebergErrorwithstatus: 0, so a singleinstanceof IcebergErrorcheck catches every failure mode.
import { IcebergRestCatalog } from 'iceberg-js'
const catalog = new IcebergRestCatalog({
baseUrl: 'https://my-catalog.example.com',
warehouse: 'my-warehouse', // optional; resolved via GET /v1/config
auth: {
type: 'bearer',
token: process.env.ICEBERG_TOKEN,
},
})
// Create a namespace
await catalog.createNamespace({ namespace: ['analytics'] })
// List namespaces (paginated)
const { namespaces, nextPageToken } = await catalog.listNamespaces({ pageSize: 50 })
// Create a table
await catalog.createTable(
{ namespace: ['analytics'] },
{
name: 'events',
schema: {
type: 'struct',
fields: [
{ id: 1, name: 'id', type: 'long', required: true },
{ id: 2, name: 'timestamp', type: 'timestamp', required: true },
{ id: 3, name: 'user_id', type: 'string', required: false },
],
'schema-id': 0,
'identifier-field-ids': [1],
},
'partition-spec': {
'spec-id': 0,
fields: [],
},
'write-order': {
'order-id': 0,
fields: [],
},
properties: {
'write.format.default': 'parquet',
},
}
)Creates a new catalog client instance.
Options:
baseUrl(string, required): Base URL of the REST catalogwarehouse(string, optional): Warehouse identifier. On first use, the client callsGET /v1/config?warehouse=…and uses the server-returnedoverrides.prefixfor all subsequent requests. This is the spec-recommended pattern (see Apache Iceberg REST spec) and is the way to address per-warehouse catalogs such as Cloudflare R2 or Tabular.catalogName(string, optional): Permanent alias forwarehouse, kept for backward compatibility. If both are provided,warehousewins.auth(AuthConfig, optional): Authentication configurationfetch(typeof fetch, optional): Custom fetch implementationaccessDelegation(AccessDelegation[], optional): Access delegation mechanisms to request from the server
Authentication types:
// No authentication
{ type: 'none' }
// Bearer token
{ type: 'bearer', token: 'your-token' }
// Custom header
{ type: 'header', name: 'X-Custom-Auth', value: 'secret' }
// Custom function
{ type: 'custom', getHeaders: async () => ({ 'Authorization': 'Bearer ...' }) }Access Delegation:
Access delegation allows the catalog server to provide temporary credentials or sign requests on your behalf:
import { IcebergRestCatalog } from 'iceberg-js'
const catalog = new IcebergRestCatalog({
baseUrl: 'https://catalog.example.com/iceberg/v1',
auth: { type: 'bearer', token: 'your-token' },
// Request vended credentials for data access
accessDelegation: ['vended-credentials'],
})
// To access vended credentials (storage-credentials, server config), use the
// *Result variants — `loadTable`/`createTable`/`registerTable` return only
// the bare `TableMetadata` and would discard credentials.
const result = await catalog.loadTableResult({ namespace: ['analytics'], name: 'events' })
// result['storage-credentials'], result.config, result.etag, result.metadataSupported delegation mechanisms:
vended-credentials: Server provides temporary credentials (e.g., AWS STS tokens) for accessing table dataremote-signing: Server signs data access requests on behalf of the client
List namespaces, optionally under a parent namespace, with cursor-based pagination.
const { namespaces } = await catalog.listNamespaces()
// namespaces: [{ namespace: ['default'] }, { namespace: ['analytics'] }]
const { namespaces: children } = await catalog.listNamespaces({
parent: { namespace: ['analytics'] },
})
// Pagination
const page1 = await catalog.listNamespaces({ pageSize: 100 })
const page2 = await catalog.listNamespaces({
pageSize: 100,
pageToken: page1.nextPageToken,
})Create a new namespace with optional properties.
await catalog.createNamespace({ namespace: ['analytics'] }, { properties: { owner: 'data-team' } })Drop a namespace. The namespace must be empty.
await catalog.dropNamespace({ namespace: ['analytics'] })Load namespace metadata and properties.
const metadata = await catalog.loadNamespaceMetadata({ namespace: ['analytics'] })
// { properties: { owner: 'data-team', ... } }Set or remove namespace properties.
await catalog.updateNamespaceProperties(
{ namespace: ['analytics'] },
{ updates: { owner: 'data-team' }, removals: ['stale_property'] }
)List tables in a namespace, with cursor-based pagination.
const { identifiers } = await catalog.listTables({ namespace: ['analytics'] })
// identifiers: [{ namespace: ['analytics'], name: 'events' }]
const page1 = await catalog.listTables({ namespace: ['analytics'] }, { pageSize: 100 })
const page2 = await catalog.listTables(
{ namespace: ['analytics'] },
{ pageSize: 100, pageToken: page1.nextPageToken }
)Create a new table.
const metadata = await catalog.createTable(
{ namespace: ['analytics'] },
{
name: 'events',
schema: {
type: 'struct',
fields: [
{ id: 1, name: 'id', type: 'long', required: true },
{ id: 2, name: 'timestamp', type: 'timestamp', required: true },
],
'schema-id': 0,
},
'partition-spec': {
'spec-id': 0,
fields: [
{
'source-id': 2,
'field-id': 1000,
name: 'ts_day',
transform: 'day',
},
],
},
}
)Load table metadata. Pass ifNoneMatch (a previous ETag) for conditional GET — returns null on 304.
const metadata = await catalog.loadTable({
namespace: ['analytics'],
name: 'events',
})
// Conditional load
const updated = await catalog.loadTable(
{ namespace: ['analytics'], name: 'events' },
{ ifNoneMatch: lastSeenEtag }
)
if (updated === null) {
// table is unchanged since lastSeenEtag
}Spec-aligned LoadTableResult wrapper exposing metadata, metadata-location, server config, storage-credentials, plus the captured ETag so you can pass it to a future loadTable call.
Commit updates to a table using the spec-aligned { requirements?, updates } shape.
const updated = await catalog.updateTable(
{ namespace: ['analytics'], name: 'events' },
{
requirements: [{ type: 'assert-current-schema-id', 'current-schema-id': 0 }],
updates: [{ action: 'set-properties', updates: { 'read.split.target-size': '134217728' } }],
}
)Drop a table from the catalog.
await catalog.dropTable({ namespace: ['analytics'], name: 'events' })All API errors throw an IcebergError with details from the server:
import { IcebergError } from 'iceberg-js'
try {
await catalog.loadTable({ namespace: ['test'], name: 'missing' })
} catch (error) {
if (error instanceof IcebergError) {
console.log(error.status) // 404
console.log(error.icebergType) // 'NoSuchTableException'
console.log(error.message) // 'Table does not exist'
}
}The library exports all relevant types:
import type {
// Identifiers
NamespaceIdentifier,
TableIdentifier,
// Schema / type system
TableSchema,
TableField,
IcebergType,
PartitionSpec,
SortOrder,
// Requests / responses
CreateTableRequest,
CommitTableRequest,
CommitTableResponse,
LoadTableResult,
LoadTableResultWithEtag,
TableMetadata,
UpdateNamespacePropertiesRequest,
UpdateNamespacePropertiesResponse,
// Method options
ListNamespacesOptions,
ListNamespacesResult,
ListTablesOptions,
ListTablesResult,
LoadTableOptions,
// Catalog config
CatalogConfig,
StorageCredential,
// Table update / requirement unions (full spec coverage)
TableUpdate,
TableRequirement,
// Auth / delegation
AuthConfig,
AccessDelegation,
} from 'iceberg-js'The following Iceberg primitive types are supported:
boolean,int,long,float,doublestring,uuid,binarydate,time,timestamp,timestamptzdecimal(precision, scale),fixed(length)
This package is built to work in all Node.js and JavaScript environments:
| Environment | Module System | Import Method | Status |
|---|---|---|---|
| Node.js ESM | "type": "module" |
import { ... } from 'iceberg-js' |
Fully supported |
| Node.js CommonJS | Default | const { ... } = require('iceberg-js') |
Fully supported |
| TypeScript ESM | module: "ESNext" |
import { ... } from 'iceberg-js' |
Full type support |
| TypeScript CommonJS | module: "CommonJS" |
import { ... } from 'iceberg-js' |
Full type support |
| Bundlers | Any | Webpack, Vite, esbuild, Rollup, etc. | Auto-detected |
| Browsers | ESM | <script type="module"> |
Modern browsers |
| Deno | ESM | import from npm: |
With npm specifier |
Package exports:
- ESM:
dist/index.mjswithdist/index.d.ts - CommonJS:
dist/index.cjswithdist/index.d.cts - Proper
exportsfield for Node.js 12+ module resolution
All scenarios are tested in CI on Node.js 22.
The library works in modern browsers that support native fetch:
import { IcebergRestCatalog } from 'iceberg-js'
const catalog = new IcebergRestCatalog({
baseUrl: 'https://public-catalog.example.com/iceberg/v1',
auth: { type: 'none' },
})
const namespaces = await catalog.listNamespaces()The library uses the global fetch by default (available in Node.js 22+ and modern browsers). You can inject a custom fetch for proxying, instrumentation, or to use a different HTTP client:
import { IcebergRestCatalog } from 'iceberg-js'
const catalog = new IcebergRestCatalog({
baseUrl: 'https://catalog.example.com/iceberg/v1',
auth: { type: 'bearer', token: 'token' },
fetch: myCustomFetch,
})# Install dependencies
pnpm install
# Build the library
pnpm run build
# Run unit tests
pnpm test
# Run integration tests (requires Docker)
pnpm test:integration
# Run integration tests with cleanup (for CI)
pnpm test:integration:ci
# Run compatibility tests (all module systems)
pnpm test:compatibility
# Format code
pnpm run format
# Lint and test
pnpm run checkIntegration tests run against a local Iceberg REST Catalog in Docker. See TESTING-DOCKER.md for details.
# Start Docker services and run integration tests
pnpm test:integration
# Or manually
docker compose up -d
npx tsx test/integration/test-local-catalog.ts
docker compose down -vThe test:compatibility script verifies the package works correctly in all JavaScript/TypeScript environments:
- Pure JavaScript ESM - Projects with
"type": "module" - Pure JavaScript CommonJS - Traditional Node.js projects
- TypeScript ESM - TypeScript with
module: "ESNext" - TypeScript CommonJS - TypeScript with
module: "CommonJS"
These tests ensure proper module resolution, type definitions, and runtime behavior across all supported environments. See test/compatibility/README.md for more details.
This project uses release-please for automated releases. Here's how it works:
-
Commit with conventional commits: Use Conventional Commits format for your commits:
feat:for new features (minor version bump)fix:for bug fixes (patch version bump)feat!:orBREAKING CHANGE:for breaking changes (major version bump)chore:,docs:,test:, etc. for non-release commits
-
Release PR is created automatically: When you push to
main, release-please creates/updates a release PR with:- Version bump in
package.json - Updated
CHANGELOG.md - Release notes
- Version bump in
-
Merge the release PR: When you're ready to release, merge the PR. This will:
- Create a GitHub release and git tag
- Automatically publish to npm with provenance (using trusted publishing, no secrets needed)
Example commits:
git commit -m "feat: add support for view operations"
git commit -m "fix: handle empty namespace list correctly"
git commit -m "feat!: change auth config structure"Contributions are welcome! Please ensure your contributions align with the library's goals and non-goals. This library aims to remain a minimal, generic client for the Iceberg REST Catalog API.