A community maintained tiny, fetch-based JavaScript and TypeScript Airtable Web and Node API client.
- 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.
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-airtableRuntime: Node 18+ (with built-in
fetch) is recommended.On Node < 18, pass your own
fetch(e.g.undici,node-fetch) in options.
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'])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.
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 / upsertclient.metadata– bases, base schema, table & view metadataclient.webhooks– create / list / refresh / delete + payload listing
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.
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)
}
}pnpm install
# Tests (Vitest)
pnpm test
pnpm test:coverage
# Lint / build
pnpm lint
pnpm buildContributions are welcome! Check CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md before contributing. Feel free to open an issue or submit a PR. ❤️
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.