Airtable как база данных: API, примеры кода, бэкап и ограничения

Обновлено:

Airtable — гибрид таблицы и реляционной базы данных с удобным интерфейсом и REST API. Его часто используют, чтобы быстро получить бэкенд без написания серверного кода: каталог, CRM, редакционный план. Ниже — как работает API, когда Airtable подходит, как делать его бэкап и когда пора переезжать на настоящую БД.


Чем Airtable отличается от Notion и обычных таблиц

Airtable позиционируется как «реляционная база данных с интерфейсом таблицы».

От Excel и Google Sheets:

  • связи между таблицами (Linked Records — аналог внешнего ключа);
  • типизированные поля (дата, число, вложение, а не просто текст);
  • формулы и rollup-вычисления по связанным записям;
  • REST API из коробки;
  • встроенные автоматизации.

От Notion:

  • Airtable — база данных, Notion — рабочее пространство с заметками;
  • API Airtable проще и предсказуемее для работы с данными;
  • длинный контент со структурой (страницы) удобнее хранить в Notion;
  • Airtable лучше для структурированных данных, Notion — для смешанного контента. Подробнее — в статье Notion как база данных.

Структура Airtable

  • Workspace — верхний уровень, рабочее пространство;
  • Base — аналог базы данных (несколько связанных таблиц);
  • Table — таблица с полями;
  • Record — строка;
  • Field — колонка с типом;
  • View — представление с фильтрами и сортировкой (данные не меняет).

Типы полей

| Тип | Описание | |---|---| | Single line text | Короткий текст | | Long text | Длинный текст, опционально с форматированием | | Number | Число с форматированием | | Currency | Денежное значение | | Date | Дата и время | | Checkbox | Булево значение | | Single/Multi select | Список значений | | Linked record | Связь с другой таблицей | | Lookup | Значение из связанной записи | | Rollup | Агрегация по связанным записям | | Formula | Вычисляемое поле | | Attachment | Файлы и изображения | | URL, Email, Phone | Специализированный текст | | Rating | Оценка (звёзды) | | Autonumber | Автоинкремент |


Airtable REST API: основы

Аутентификация

Airtable использует Personal Access Tokens (PAT). Создайте токен на airtable.com/create/tokens только с нужными правами (scopes) и только к нужным базам.

# Получить записи таблицы
curl "https://api.airtable.com/v0/$BASE_ID/Products" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN"

ID базы виден в URL: airtable.com/appXXXXXXXX/tblXXXXXXXX/...

Получение записей

Формулы и параметры нужно URL-кодировать — проще поручить это curl:

# Фильтр через filterByFormula
curl -G "https://api.airtable.com/v0/$BASE_ID/Posts" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  --data-urlencode "filterByFormula=AND({Status}='Published',{Category}='Tech')"

# Сортировка и лимит
curl -G "https://api.airtable.com/v0/$BASE_ID/Posts" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  --data-urlencode "sort[0][field]=Date" \
  --data-urlencode "sort[0][direction]=desc" \
  --data-urlencode "maxRecords=10"

# Пагинация: offset из предыдущего ответа
curl -G "https://api.airtable.com/v0/$BASE_ID/Posts" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  --data-urlencode "pageSize=100" \
  --data-urlencode "offset=$OFFSET"

Создание записи

curl -X POST "https://api.airtable.com/v0/$BASE_ID/Products" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "Name": "Новый товар",
      "Price": 1500,
      "Category": "Electronics",
      "In Stock": true
    }
  }'

Обновление

# PATCH — обновить только указанные поля
curl -X PATCH "https://api.airtable.com/v0/$BASE_ID/Products/$RECORD_ID" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fields": {"Price": 1200}}'

Airtable на JavaScript (Node.js)

Официальная библиотека

npm install airtable
const Airtable = require('airtable')

// Только на сервере: токен не должен попасть в браузер
const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN })
  .base(process.env.AIRTABLE_BASE_ID)

async function getAllProducts() {
  const records = []

  await base('Products').select({
    filterByFormula: "{Status} = 'Active'",
    sort: [{ field: 'Name', direction: 'asc' }]
  }).eachPage((pageRecords, fetchNextPage) => {
    pageRecords.forEach(record => {
      records.push({
        id: record.id,
        name: record.get('Name'),
        price: record.get('Price'),
        category: record.get('Category'),
        imageUrl: record.get('Image')?.[0]?.url
      })
    })
    fetchNextPage()
  })

  return records
}

async function createOrder(order) {
  return base('Orders').create({
    'Customer Name': order.name,
    'Email': order.email,
    'Product': [order.productId],  // Linked record — массив ID
    'Status': 'New',
    'Date': new Date().toISOString()
  })
}

async function updateOrderStatus(recordId, status) {
  return base('Orders').update(recordId, { 'Status': status })
}

async function deleteRecord(recordId) {
  return base('Products').destroy(recordId)
}

Использование в Next.js Route Handler

// app/api/products/route.ts
import Airtable from 'airtable'
import { NextResponse } from 'next/server'

const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN })
  .base(process.env.AIRTABLE_BASE_ID!)

export async function GET() {
  try {
    const records: { id: string; name: unknown; price: unknown }[] = []

    await base('Products').select({
      filterByFormula: "{Status} = 'Active'",
      sort: [{ field: 'Name', direction: 'asc' }]
    }).eachPage((pageRecords, fetchNextPage) => {
      pageRecords.forEach(record => {
        records.push({
          id: record.id,
          name: record.get('Name'),
          price: record.get('Price'),
        })
      })
      fetchNextPage()
    })

    return NextResponse.json(records)
  } catch {
    return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 })
  }
}

export async function POST(request: Request) {
  const body = await request.json()

  const record = await base('Orders').create({
    'Name': body.name,
    'Email': body.email,
    'Product': [body.productId],
  })

  return NextResponse.json({ id: record.id })
}

Airtable на Python

pip install pyairtable
import os
from pyairtable import Api

api = Api(os.environ["AIRTABLE_TOKEN"])
table = api.table(os.environ["AIRTABLE_BASE_ID"], "Products")

# Все записи с фильтром (пагинацию pyairtable делает сам)
records = table.all(
    formula="AND({Status}='Active', {Price}<1000)",
    sort=["Name"],
)

for record in records:
    print(f"{record['fields']['Name']}: {record['fields'].get('Price', 0)} руб.")

new_record = table.create({"Name": "Новый товар", "Price": 999, "Status": "Active"})
table.update(new_record["id"], {"Price": 849})
table.delete(new_record["id"])

# API принимает до 10 записей за запрос — pyairtable сам разбивает список на пачки
table.batch_create([
    {"Name": "Товар 1", "Price": 100},
    {"Name": "Товар 2", "Price": 200},
    {"Name": "Товар 3", "Price": 300},
])

Практические кейсы Airtable как backend

Кейс 1: каталог товаров для лендинга

Схема:

  • Products: Name, Price, Description, Images (attachments), Category (single select), Featured (checkbox), Status (single select);
  • Categories: Name, Slug, Description.

Next.js получает данные с кэшем на час:

const data = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Products`, {
  headers: { Authorization: `Bearer ${process.env.AIRTABLE_TOKEN}` },
  next: { revalidate: 3600 }
})

Учтите: ссылки на вложения, которые возвращает API, временные. Картинки для сайта лучше копировать в своё хранилище, а не вставлять ссылку Airtable напрямую.

Кейс 2: форма обратной связи → CRM в Airtable

// Серверный обработчик формы
async function handleContactForm(formData) {
  await base('Leads').create({
    'Name': formData.name,
    'Email': formData.email,
    'Message': formData.message,
    'Source': 'Website',
    'Status': 'New',
    'Created At': new Date().toISOString()
  })
}

Заявки с сайта появляются в Airtable, менеджеры работают с ними как в CRM.

Кейс 3: редакционный план и блог

Таблица Posts: Title, Slug, Status (Draft/Review/Published), Author (linked), PublishDate, Tags (multi-select), Content (long text).

def get_published_posts():
    records = table.all(
        formula="AND({Status}='Published', IS_BEFORE({PublishDate}, TODAY()))",
        sort=["-PublishDate"],  # минус — сортировка по убыванию
    )
    return [
        {
            "slug": r["fields"]["Slug"],
            "title": r["fields"]["Title"],
            "date": r["fields"]["PublishDate"],
            "tags": r["fields"].get("Tags", []),
        }
        for r in records
    ]

Ограничения Airtable

Лимиты зависят от тарифа и периодически меняются — ниже ориентиры на момент написания, проверяйте актуальные значения на странице тарифов Airtable.

| Ограничение | Free | Team | |---|---|---| | Записей на базу | 1 000 | 50 000 | | Вложения на базу | 1 ГБ | 20 ГБ | | История ревизий | 2 недели | 1 год |

Важные ограничения API:

  • rate limit — 5 запросов в секунду на базу, при превышении API возвращает 429; без кэширования сайт с трафиком упрётся в лимит;
  • на бесплатном плане есть ещё и месячный лимит API-вызовов;
  • не больше 100 записей за запрос, дальше — пагинация через offset;
  • формулы передаются в URL и должны быть закодированы.

Бэкап и экспорт Airtable

Все данные живут в облаке Airtable за рубежом: записи, вложения и история ревизий. Встроенные снимки базы и история изменений помогают откатить ошибку, но хранятся у того же провайдера — при блокировке аккаунта, проблемах с оплатой или случайном удалении базы через API они не помогут.

Ручной вариант — CSV из представления (меню view → Download CSV). Ограничения: одна таблица за раз, только видимые поля, связи выгружаются текстом, вложения — временными ссылками.

Надёжнее — выгрузка через API по расписанию. Токену нужны права data.records:read и schema.bases:read:

# airtable_dump.py — все таблицы базы в JSON и SQLite
import datetime, json, os, sqlite3
from pyairtable import Api

base = Api(os.environ["AIRTABLE_TOKEN"]).base(os.environ["AIRTABLE_BASE_ID"])
stamp = datetime.date.today().isoformat()
db = sqlite3.connect(f"airtable-{stamp}.db")
dump = {}

for table in base.tables():
    records = table.all()
    dump[table.name] = records
    db.execute(f'CREATE TABLE "{table.name}" (id TEXT PRIMARY KEY, created TEXT, fields TEXT)')
    db.executemany(
        f'INSERT INTO "{table.name}" VALUES (?, ?, ?)',
        [(r["id"], r["createdTime"], json.dumps(r["fields"], ensure_ascii=False)) for r in records],
    )

db.commit()
db.close()
with open(f"airtable-{stamp}.json", "w", encoding="utf-8") as f:
    json.dump(dump, f, ensure_ascii=False, indent=2)

Вложения скачивайте тем же скриптом сразу — ссылки протухают. Запускайте по cron раз в сутки и отправляйте файл за пределы Airtable: в S3 или в dbsend. SQLite-файл удобен тем, что dbsend покажет число записей по таблицам в каждой версии и diff между ними — сразу видно, если кто-то массово удалил строки:

python airtable_dump.py
dbsend backup "airtable-$(date +%F).db" -d "$DBSEND_DATABASE_ID" -e sqlite -l "airtable-$(date +%F)"

Подробнее о хранении JSON в SQLite — в статье JSON как база данных, о версиях и алертах — на странице бэкапы SQLite. Раз в квартал проверяйте, что из дампа можно восстановить данные: залейте пару таблиц в пустую тестовую базу через batch_create — чек-лист в статье как проверить, что бэкап рабочий.

Когда переезжать на SQLite или PostgreSQL: упираетесь в лимит записей или rate limit, нужны транзакции и сложные запросы, персональные данные попадают под 152-ФЗ. Дамп выше — готовая отправная точка для миграции; про выбор СУБД — в статье SQLite, PostgreSQL, MySQL или NoSQL.


No-code инструменты поверх Airtable

  • Softr (softr.io) — веб-приложения и клиентские порталы с авторизацией;
  • Noloco (noloco.io) — внутренние инструменты;
  • Glide (glideapps.com) — мобильные и веб-приложения.

Airtable vs Notion как база данных

| Критерий | Airtable | Notion | |---|---|---| | Реляционные связи | Сильнее (linked records, lookup, rollup) | Есть relations и rollup, но проще | | API | Проще и предсказуемее | Сложная структура ответа | | Длинный контент внутри записи | Только текстовое поле | Полноценные страницы | | Редактирование нетехниками | Хорошо | Отлично | | Автоматизации | Сильные встроенные | Проще | | Оплата из России | Иностранная карта | Иностранная карта |


Безопасность: не раскрывайте токен

Самая частая ошибка — токен Airtable в браузерном JavaScript:

// ОПАСНО — токен виден всем через DevTools
const data = await fetch(`https://api.airtable.com/v0/${BASE_ID}/Table`, {
  headers: { Authorization: `Bearer ${token}` }
})

Обращайтесь к Airtable только из серверного кода (Route Handler, API-эндпоинт, серверная функция), а токен держите в переменных окружения.


Частые вопросы

Можно ли использовать Airtable как основную БД приложения?

Для лендингов, MVP, внутренних инструментов и CRM малого бизнеса — да. Для приложений с большим числом пользователей, высокой нагрузкой или транзакциями — нет, переходите на PostgreSQL.

Airtable хранит данные в России?

Нет, данные хранятся за рубежом. Если обрабатываете персональные данные россиян, 152-ФЗ требует их первичной записи и хранения в России — Airtable для этого не подходит. Рассмотрите self-hosted open-source аналоги на российском сервере или обычную БД.

Как сделать полнотекстовый поиск в Airtable через API?

Формулы поддерживают SEARCH() и FIND(), но это медленно и упирается в rate limit. Для нормального поиска выгружайте данные на свой сервер и ищите там: SQLite FTS5, PostgreSQL + pg_trgm или поисковый движок.

Есть ли альтернатива Airtable, которую можно развернуть в России?

Open-source аналоги, которые можно поставить на российский VPS: NocoDB, Baserow, Teable. Под капотом у них обычная БД (часто PostgreSQL), так что бэкапы делаются стандартным pg_dump.

Как сделать Airtable быстрым для сайта с трафиком?

Кэшируйте данные на своём сервере или в Redis и обновляйте кэш по расписанию или по событию из Airtable Automations. Не запрашивайте Airtable при каждом запросе пользователя.

Airtable поддерживает вебхуки?

Да: есть Webhooks API для подписки на изменения базы, а в Automations можно отправить HTTP-запрос при изменении записи. Используйте это для инвалидации кэша или запуска задач в своём приложении.