TL;DR
Notion SDK provides programmatic access to Notion databases, pages, and blocks — query databases, create pages, manage content, build integrations with the Notion workspace. Airtable API turns Airtable bases into a backend — CRUD operations, formulas, attachments, views, webhook automations, relational data. Google Sheets API treats spreadsheets as a database — read/write cells, formatting, charts, widely used for quick data backends and reporting. In 2026: Notion SDK for content and knowledge base automation, Airtable API for relational data and app backends, Google Sheets API for spreadsheet automation and reporting.
Key Takeaways
- Notion: @notionhq/client ~300K weekly downloads — databases, pages, blocks, rich content
- Airtable: airtable ~200K weekly downloads — relational data, views, formulas, attachments
- Google Sheets: googleapis ~5M weekly downloads — cells, ranges, formatting, charts
- Notion excels at rich content (pages, blocks, embeds)
- Airtable excels at relational data (linked records, views)
- Google Sheets is the most accessible (everyone knows spreadsheets)
Notion SDK
Notion SDK — Notion API client:
Query database
import { Client } from "@notionhq/client"
const notion = new Client({ auth: process.env.NOTION_TOKEN })
// Query a database:
const response = await notion.databases.query({
database_id: "abc123...",
filter: {
and: [
{
property: "Status",
select: { equals: "Published" },
},
{
property: "Tags",
multi_select: { contains: "react" },
},
],
},
sorts: [
{ property: "Date", direction: "descending" },
],
page_size: 10,
})
// Parse results:
response.results.forEach((page: any) => {
const title = page.properties.Title.title[0]?.plain_text
const status = page.properties.Status.select?.name
const date = page.properties.Date.date?.start
console.log(`${title} — ${status} (${date})`)
})
Create page
import { Client } from "@notionhq/client"
const notion = new Client({ auth: process.env.NOTION_TOKEN })
// Create a page in a database:
const page = await notion.pages.create({
parent: { database_id: "abc123..." },
properties: {
Title: {
title: [{ text: { content: "React 19 Released" } }],
},
Status: {
select: { name: "Draft" },
},
Tags: {
multi_select: [
{ name: "react" },
{ name: "release" },
],
},
Date: {
date: { start: "2026-03-09" },
},
Downloads: {
number: 25000000,
},
},
// Page content (blocks):
children: [
{
object: "block",
type: "heading_2",
heading_2: {
rich_text: [{ text: { content: "Overview" } }],
},
},
{
object: "block",
type: "paragraph",
paragraph: {
rich_text: [{
text: { content: "React 19 introduces new features..." },
}],
},
},
{
object: "block",
type: "code",
code: {
rich_text: [{ text: { content: "npm install react@19" } }],
language: "bash",
},
},
],
})
Update and search
import { Client } from "@notionhq/client"
const notion = new Client({ auth: process.env.NOTION_TOKEN })
// Update page properties:
await notion.pages.update({
page_id: "page-123...",
properties: {
Status: { select: { name: "Published" } },
Date: { date: { start: new Date().toISOString().split("T")[0] } },
},
})
// Search across workspace:
const search = await notion.search({
query: "react",
filter: { property: "object", value: "page" },
sort: { direction: "descending", timestamp: "last_edited_time" },
})
search.results.forEach((result: any) => {
console.log(result.id, result.url)
})
// Append blocks to page:
await notion.blocks.children.append({
block_id: "page-123...",
children: [
{
object: "block",
type: "callout",
callout: {
rich_text: [{ text: { content: "Updated with latest data!" } }],
icon: { emoji: "📊" },
},
},
],
})
Airtable API
Airtable — relational database API:
Query records
import Airtable from "airtable"
const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN })
.base(process.env.AIRTABLE_BASE_ID!)
// Query records:
const records = await base("Packages")
.select({
filterByFormula: "AND({Status} = 'Active', {Downloads} > 1000000)",
sort: [{ field: "Downloads", direction: "desc" }],
maxRecords: 10,
view: "Published",
})
.all()
records.forEach((record) => {
console.log({
id: record.id,
name: record.get("Name"),
downloads: record.get("Downloads"),
tags: record.get("Tags"),
status: record.get("Status"),
})
})
Create and update records
import Airtable from "airtable"
const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN })
.base(process.env.AIRTABLE_BASE_ID!)
// Create records (batch up to 10):
const created = await base("Packages").create([
{
fields: {
Name: "react",
Description: "UI library for building interfaces",
Downloads: 25000000,
Tags: ["frontend", "ui"],
Status: "Active",
Website: "https://react.dev",
},
},
{
fields: {
Name: "vue",
Description: "Progressive JavaScript framework",
Downloads: 5000000,
Tags: ["frontend", "ui"],
Status: "Active",
},
},
])
// Update records:
await base("Packages").update([
{
id: "rec123...",
fields: {
Downloads: 26000000,
"Last Updated": new Date().toISOString(),
},
},
])
// Upsert (update or create):
await base("Packages").update(
[{ fields: { Name: "react", Downloads: 26000000 } }],
{ performUpsert: { fieldsToMergeOn: ["Name"] } }
)
Linked records and formulas
import Airtable from "airtable"
const base = new Airtable({ apiKey: process.env.AIRTABLE_TOKEN })
.base(process.env.AIRTABLE_BASE_ID!)
// Create with linked records:
await base("Versions").create([
{
fields: {
Version: "19.0.0",
Package: ["rec123..."], // Link to Packages table
"Release Date": "2026-03-09",
"Breaking Changes": true,
},
},
])
// Query with linked records expanded:
const packages = await base("Packages")
.select({
fields: ["Name", "Downloads", "Latest Version"],
// "Latest Version" is a lookup/rollup from Versions table
})
.all()
// Computed fields are read-only:
// - Formulas: calculated server-side
// - Rollups: aggregate linked records
// - Lookups: pull fields from linked records
Webhooks and automations
// Airtable webhook (receive changes):
// POST /api/webhooks/airtable
app.post("/api/webhooks/airtable", async (req, res) => {
const { base, table, record, action } = req.body
if (action === "create" && table === "Packages") {
// New package added — trigger analysis:
await analyzePackage(record.fields.Name)
}
if (action === "update" && record.fields.Status === "Published") {
// Published — deploy content:
await deployContent(record)
}
res.sendStatus(200)
})
Google Sheets API
Google Sheets API — spreadsheet access:
Read data
import { google } from "googleapis"
const auth = new google.auth.GoogleAuth({
keyFile: "service-account.json",
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
})
const sheets = google.sheets({ version: "v4", auth })
// Read a range:
const response = await sheets.spreadsheets.values.get({
spreadsheetId: process.env.SHEET_ID!,
range: "Packages!A2:E", // Skip header row
})
const rows = response.data.values || []
rows.forEach(([name, downloads, version, tags, status]) => {
console.log({ name, downloads: Number(downloads), version, tags, status })
})
// Read multiple ranges:
const multi = await sheets.spreadsheets.values.batchGet({
spreadsheetId: process.env.SHEET_ID!,
ranges: ["Packages!A2:E", "Downloads!A2:C", "Metadata!A1:B10"],
})
multi.data.valueRanges?.forEach((range) => {
console.log(range.range, range.values?.length, "rows")
})
Write data
import { google } from "googleapis"
const sheets = google.sheets({ version: "v4", auth })
// Append rows:
await sheets.spreadsheets.values.append({
spreadsheetId: process.env.SHEET_ID!,
range: "Packages!A:E",
valueInputOption: "USER_ENTERED",
requestBody: {
values: [
["react", 25000000, "19.0.0", "frontend,ui", "Active"],
["vue", 5000000, "3.5.0", "frontend,ui", "Active"],
],
},
})
// Update specific cells:
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.SHEET_ID!,
range: "Packages!B2", // Downloads column, row 2
valueInputOption: "RAW",
requestBody: {
values: [[26000000]],
},
})
// Batch update multiple ranges:
await sheets.spreadsheets.values.batchUpdate({
spreadsheetId: process.env.SHEET_ID!,
requestBody: {
valueInputOption: "USER_ENTERED",
data: [
{ range: "Packages!B2", values: [[26000000]] },
{ range: "Packages!B3", values: [[5500000]] },
{ range: "Metadata!B1", values: [[new Date().toISOString()]] },
],
},
})
Formatting and sheets management
import { google } from "googleapis"
const sheets = google.sheets({ version: "v4", auth })
// Create new sheet:
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.SHEET_ID!,
requestBody: {
requests: [
{
addSheet: {
properties: {
title: "March 2026 Report",
gridProperties: { rowCount: 1000, columnCount: 10 },
},
},
},
],
},
})
// Format cells (bold header, background color):
await sheets.spreadsheets.batchUpdate({
spreadsheetId: process.env.SHEET_ID!,
requestBody: {
requests: [
{
repeatCell: {
range: { sheetId: 0, startRowIndex: 0, endRowIndex: 1 },
cell: {
userEnteredFormat: {
backgroundColor: { red: 0.23, green: 0.51, blue: 0.96 },
textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } },
},
},
fields: "userEnteredFormat(backgroundColor,textFormat)",
},
},
],
},
})
Feature Comparison
| Feature | Notion SDK | Airtable API | Google Sheets API |
|---|---|---|---|
| Data model | Pages + databases | Tables + records | Sheets + cells |
| Rich content | ✅ (blocks, embeds) | ❌ (fields only) | ❌ (cells only) |
| Relational data | ✅ (relations) | ✅ (linked records) | ❌ (manual) |
| Formulas | ✅ (database formulas) | ✅ (powerful) | ✅ (spreadsheet) |
| Attachments | ✅ (files, images) | ✅ (attachments) | ❌ (links only) |
| Webhooks | ❌ | ✅ (automations) | ✅ (Apps Script) |
| Real-time | ❌ | ❌ | ❌ |
| Rate limits | 3 req/s | 5 req/s | 60 req/min |
| Row limit | Unlimited | 50K/base (free) | 10M cells |
| TypeScript | ✅ | ✅ (@types) | ✅ |
| Auth | API token | API token | OAuth2/Service Account |
| Free tier | Generous | 1,200 records | 15 GB Drive |
| Pricing | Per-user | Per-seat | Free/Workspace |
Evaluating No-Code Databases as Application Backends
Using Notion, Airtable, or Google Sheets as an application backend is a pragmatic choice for early-stage products where content management velocity matters more than performance or data modeling precision. Each platform provides a visual interface that non-technical team members can use to add, edit, and organize data without deploying code — a significant productivity advantage during product discovery phases. The operational risks are equally important to acknowledge: API rate limits create hard ceilings on request throughput, data export and migration are non-trivial, and the visual interfaces optimized for human editing are not optimized for programmatic data management at scale. Use these platforms as backends for data that changes at human speed (content, configuration, editorial data) rather than data that changes at application speed (user actions, events, analytics).
Rate Limiting and Production Reliability
Production integrations with all three APIs require defensive rate limit handling. Notion's 3 requests/second limit is the most restrictive — implement exponential backoff with jitter on 429 responses, and consider a request queue with a configurable concurrency limit to stay safely under the limit during bulk operations. Airtable's 5 requests/second limit is per base, not per account, so applications accessing multiple bases can parallelize across bases. Google Sheets' 60 requests/minute limit is per user per project in the Google Cloud Console — for server-side integrations using a service account, this limit applies to all operations from that service account, making batched batchGet and batchUpdate calls essential for efficient bulk operations.
Data Modeling Trade-offs
The choice between these three APIs often comes down to how well their data models fit your application's domain. Notion's block-based content model is unrivaled for rich text content — paragraphs, headers, code blocks, callouts, and embeds are first-class citizens rather than text stored in a cell. However, Notion's relational capabilities are limited: linked database relations work well for simple one-to-one lookups but become unwieldy for many-to-many relationships spanning multiple databases. Airtable's relational model supports proper linked records with rollup and lookup computed fields, making it genuinely useful as an application backend for data-driven tools. Google Sheets' flat cell model requires manual schema enforcement — column naming conventions and data validation rules must be maintained by convention rather than enforced by the API.
TypeScript Integration
The Google Sheets API via googleapis has extensive TypeScript declarations generated from the API discovery document, but the deeply nested response types can be verbose — response.data.values?.forEach(row => ...) requires null-coalescing at every level because the API spec allows most fields to be absent. Airtable's @types/airtable package covers the main API surface with reasonable type coverage, but the record.get("FieldName") pattern returns Airtable.FieldSet[string] which is string | number | boolean | string[] | object | Attachment[] | undefined — requiring explicit casting or runtime type narrowing. The Notion SDK's TypeScript types are the most precise: property types are typed as discriminated unions keyed by the property type string, enabling type-safe property access after narrowing on property.type.
Webhook and Automation Patterns
Airtable's webhook support is the strongest of the three for event-driven architectures. Airtable sends webhook notifications within seconds of record changes, supports filtering by table and field, and provides a cursor-based event retrieval API that allows catching up on missed events after downtime. Notion's API lacks native webhooks — most integrations poll for changes using search or databases.query with a last_edited_time filter, which adds latency and wastes API quota on unchanged content. Google Sheets supports webhooks via Apps Script triggers and the newer Push Notifications API, but the push notification system requires domain verification and has strict delivery requirements. For real-time use cases, Airtable is the clear choice; for scheduled batch processing, all three work equally well.
Caching and Offline Patterns
Server-side applications that render Notion, Airtable, or Sheets content should implement aggressive caching to stay within rate limits and reduce latency. Store fetched data in Redis with a TTL matching your acceptable staleness threshold — 60 seconds for frequently updated data, 1 hour for infrequently changed content. Revalidate on a background cron job rather than on cache miss to avoid slow page loads when the cache expires. For static site generation with Next.js ISR, fetch API data at build time and revalidate using webhook-triggered on-demand ISR when content changes — this pattern provides instant page loads from the CDN while maintaining fresh content within seconds of publishing changes in the CMS.
Data Synchronization and Conflict Resolution
When using no-code APIs as application backends, keeping data synchronized between the external service and your application state becomes a critical architectural concern. Notion's webhook API allows subscribing to page and database changes, but the webhook payload contains only the changed object's ID — your application must then re-fetch the full object via the SDK to get the current state. This two-step pattern (webhook notification + re-fetch) adds latency and introduces a race condition window where the re-fetch may itself be stale if another change occurred between the webhook and the API call. Airtable's automation webhooks are more detailed, including the changed field values in the payload, reducing the need for re-fetching. For Google Sheets, the Drive API's change notification feature delivers push notifications when a spreadsheet is modified, but the notification only indicates that a change occurred — not which cells changed — requiring a full re-read of the sheet range. Build idempotent synchronization handlers that tolerate duplicate or out-of-order webhook deliveries, using the object's last_edited_time (Notion) or record's modification timestamp to detect and ignore stale updates.
When to Use Each
Use Notion SDK if:
- Building integrations with Notion workspaces
- Need rich content management (pages, blocks, embeds)
- Building knowledge bases or content pipelines
- Want to automate Notion databases programmatically
Use Airtable API if:
- Need a relational database with a visual UI
- Building app backends with linked records and views
- Want powerful formulas and automations
- Need attachments and file management
Use Google Sheets API if:
- Need spreadsheet automation and reporting
- Building data pipelines that output to spreadsheets
- Want the most familiar interface for non-technical users
- Need integration with the Google Workspace ecosystem
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on @notionhq/client v2.x, airtable v0.12.x, and googleapis v140.x.
Compare API client and developer tooling on PkgPulse →
See also: AVA vs Jest and Medusa vs Saleor vs Vendure 2026, Cal.com vs Calendly vs Nylas (2026).