Skip to content

A tiny, fetch-based JavaScript and TypeScript Airtable Web API client

License

Notifications You must be signed in to change notification settings

ZL-Asica/TS-Airtable

Airtable TS

npm version License Coverage Node.js pnpm Version | VitePress Vitest Eslint

A community maintained tiny, fetch-based JavaScript and TypeScript Airtable Web and Node API client.

📚 Documentation

  • First-class TypeScript
  • Modern, promise-only API
  • Airtable.js-style façade (Airtable.configure(...); Airtable.base(...))
  • Records + metadata + webhooks
  • Built-in retries with exponential backoff
  • Support Node, Web, Edge, and even more environments
  • Built-in pluggable record caching (with a built-in in-memory store)
  • (WIP) Built-in logging interface to trigger logging when needed

It’s meant to be boring, predictable glue around Airtable’s HTTP API — no magic.

Installation

pnpm add ts-airtable
# or
npm install ts-airtable
# or
yarn add ts-airtable
# or
bun add ts-airtable
# or deno
deno add npm:ts-airtable

Runtime: Node 18+ (with built-in fetch) is recommended.

On Node < 18, pass your own fetch (e.g. undici, node-fetch) in options.

Quick start (Airtable.js-style façade)

High-level façade, compatible with the official airtable.js style:

import Airtable from 'ts-airtable'

interface Task {
  Name: string
  Status?: 'Todo' | 'Doing' | 'Done'
}

Airtable.configure({
  apiKey: process.env.AIRTABLE_API_KEY!,
  // endpointUrl?: 'https://api.airtable.com'
  // fetch?: typeof fetch
  // maxRetries?: number
  // retryInitialDelayMs?: number
  // retryOnStatuses?: number[]
})

const base = Airtable.base<Task>(process.env.AIRTABLE_BASE_ID!)

// List all records in a view
const records = await base('Tasks').select({ view: 'Grid view' }).all()
console.log(records[0].fields.Name)

// First page only
const firstPage = await base('Tasks').select({ pageSize: 50 }).firstPage()

// Single record
const rec = await base('Tasks').find('recXXXXXXXXXXXXXX')

// Create / update / delete
await base('Tasks').create([{ fields: { Name: 'Write docs', Status: 'Todo' } }])
await base('Tasks').update([{ id: 'rec1', fields: { Status: 'Done' } }])
await base('Tasks').destroy('rec1')
await base('Tasks').destroyMany(['rec2', 'rec3'])

Shape of the façade

Airtable.configure({
  apiKey: string,
  endpointUrl?: string,
  fetch?: typeof fetch,
  maxRetries?: number,
  retryInitialDelayMs?: number,
  retryOnStatuses?: number[],
  // recordsCache?: AirtableRecordsCacheOptions (optional global records cache)
})

const base = Airtable.base<MyFields>(baseId /*, {
  // Optional per-base overrides (currently: recordsCache)
  // recordsCache?: AirtableRecordsCacheOptions
}*/)

// base is a function: base(tableName) -> table helper
base.id     // baseId
base.client // underlying AirtableClient instance

const table = base('Table name')
table.select(params).all()
table.select(params).firstPage()
table.find(recordId, params?)
table.create(records, options?)
table.update(records, options?)
table.updateRecord(recordId, fields, options?)
table.destroy(recordId)
table.destroyMany(recordIds)

If you only need to do record listing + create / update / delete, the façade is all you need.

Low-level client (AirtableClient)

If you prefer a direct client (and full surface area: records + metadata + webhooks):

import { AirtableClient } from 'ts-airtable'

interface Task {
  Name: string
  Status?: 'Todo' | 'Doing' | 'Done'
}

const client = new AirtableClient<Task>({
  apiKey: process.env.AIRTABLE_API_KEY!,
  baseId: process.env.AIRTABLE_BASE_ID!,
  // endpointUrl?: string
  // fetch?: typeof fetch
  // maxRetries?: number
  // retryInitialDelayMs?: number
  // retryOnStatuses?: number[]
})

const page = await client.records.listRecords('Tasks', {
  view: 'Grid view',
  pageSize: 50,
})

const schema = await client.metadata.getBaseSchema()
const webhooks = await client.webhooks.listWebhooks()
  • client.records – list / get / create / update / delete / upsert
  • client.metadata – bases, base schema, table & view metadata
  • client.webhooks – create / list / refresh / delete + payload listing

Optional record caching (overview)

Record caching for reads (listRecords, listAllRecords, iterateRecords, getRecord) is opt-in and configured via recordsCache.

A simple in-process LRU+TTL store is built in: InMemoryCacheStore.

import Airtable, { InMemoryCacheStore } from 'ts-airtable'

Airtable.configure({
  apiKey: process.env.AIRTABLE_API_KEY!,
  recordsCache: {
    store: new InMemoryCacheStore(),
    defaultTtlMs: 30_000,
  },
})

const base = Airtable.base<Task>(process.env.AIRTABLE_BASE_ID!)
const records = await base('Tasks').select({ view: 'Grid view' }).all()

You can also configure caching directly on the low-level client:

import { AirtableClient, InMemoryCacheStore } from 'ts-airtable'

const client = new AirtableClient<Task>({
  apiKey: process.env.AIRTABLE_API_KEY!,
  baseId: process.env.AIRTABLE_BASE_ID!,
  recordsCache: {
    store: new InMemoryCacheStore(),
    defaultTtlMs: 60_000,
  },
})

For details on key strategy, invalidation, custom stores (Redis / KV, etc.) and when you should use caching, see the Caching documentation.

Error handling

All non-2xx responses are wrapped in AirtableError:

import { AirtableError, isAirtableError } from 'ts-airtable'

try {
  await client.records.listRecords('Tasks')
} catch (err) {
  if (isAirtableError(err)) {
    console.error('Airtable error', err.status, err.type, err.message)
  } else {
    console.error('Unexpected error', err)
  }
}

Development

pnpm install

# Tests (Vitest)
pnpm test
pnpm test:coverage

# Lint / build
pnpm lint
pnpm build

Contributing

Contributions are welcome! Check CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md before contributing. Feel free to open an issue or submit a PR. ❤️

License

This is an unofficial community library and is not endorsed, sponsored, or affiliated with Airtable. Airtable is a registered trademark of Formagrid Inc.

This project is licensed under the MIT License.

About

A tiny, fetch-based JavaScript and TypeScript Airtable Web API client

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published