A TypeScript ORM for Notion databases. Query, create, update, and delete records with type safety -- no more wrestling with deeply nested API responses.
https://interactive-inc.github.io/open-notion-client/
Working directly with the Notion API is painful. A simple text value comes buried in layers of nested objects:
{
"properties": {
"Name": {
"type": "title",
"title": [
{
"text": { "content": "Hello World" },
"annotations": { "bold": false, "italic": false }
}
]
}
}
}notion-client flattens this to { name: "Hello World" } and gives you a clean, familiar ORM interface.
- Type-safe schema definitions with full TypeScript inference
- Intuitive CRUD operations that feel like Prisma or Drizzle
- Powerful query builder with filters, sorting, and pagination
- Automatic conversion between Notion's complex JSON and simple values
- Markdown support for page content with bidirectional conversion
- Built-in memory cache to reduce API calls
- Safe mode (
.safe) that returnsT | Errorinstead of throwing - Automatic retry with exponential backoff for rate-limited requests
- Batch operations with partial failure handling
bun add @interactive-inc/notion-client @notionhq/clientimport { Client } from "@notionhq/client"
import { NotionTable } from "@interactive-inc/notion-client"
const client = new Client({ auth: process.env.NOTION_TOKEN })
const tasks = new NotionTable({
client,
dataSourceId: "your-database-id",
properties: {
title: { type: "title" },
status: { type: "select", options: ["todo", "doing", "done"] },
priority: { type: "number" },
tags: { type: "multi_select", options: ["bug", "feature"] },
} as const,
})const task = await tasks.create({
properties: {
title: "Implement new feature",
status: "todo",
priority: 1,
tags: ["feature"],
},
})// Find multiple records
const allTasks = await tasks.findMany({
where: { status: "doing" },
sorts: [{ field: "priority", direction: "desc" }],
limit: 10,
})
// Find one record
const task = await tasks.findOne({
where: { title: "Implement new feature" },
})
// Find by ID
const task = await tasks.findById("page-id")// Update one
await tasks.update(task.id, {
properties: { status: "done" },
})
// Update with markdown body
await tasks.update(task.id, {
properties: { status: "done" },
body: "# Done\n\nCompleted this task.",
})
// Clear page body
await tasks.update(task.id, {
properties: {},
body: null,
})
// Update many (returns BatchResult)
const result = await tasks.updateMany({
where: { status: "todo" },
update: { properties: { status: "doing" } },
})
console.log(result.succeeded.length, result.failed.length)// Archive one
await tasks.delete(task.id)
// Archive many (returns BatchResult)
const result = await tasks.deleteMany({ status: "done" })
// Restore archived
await tasks.restore(task.id)await tasks.upsert({
where: { title: "Daily standup" },
create: {
properties: { title: "Daily standup", status: "todo" },
},
update: {
properties: { status: "in_progress" },
},
})const results = await tasks.findMany({
where: {
priority: { greater_than_or_equal_to: 5 },
status: { does_not_equal: "done" },
tags: { contains: "bug" },
},
})Supported operators: equals, does_not_equal, contains, does_not_contain, starts_with, ends_with, greater_than, less_than, greater_than_or_equal_to, less_than_or_equal_to, before, after, on_or_before, on_or_after, is_empty, is_not_empty
const results = await tasks.findMany({
where: {
or: [
{ status: "todo" },
{
and: [{ priority: { greater_than_or_equal_to: 5 } }, { tags: { contains: "urgent" } }],
},
],
},
})Supported Notion property types and their TypeScript mappings:
title--string-- Page title (required)rich_text--string-- Text contentnumber--number | null-- Numeric valueselect--string | null-- Single selectmulti_select--string[]-- Multiple selectstatus--string | null-- Status fieldcheckbox--boolean-- Booleanurl--string | null-- URL stringemail--string | null-- Email addressphone_number--string | null-- Phone numberdate--{ start, end, timeZone } | null-- Date valuefiles--Array<{ url }>-- File attachmentspeople--Array<{ id }>-- User referencesrelation--string[]-- Relationsformula-- Read-only computed valuecreated_time/last_edited_time-- Read-only timestampscreated_by/last_edited_by-- Read-only user references
Create pages with Markdown content that automatically converts to Notion blocks:
const post = await posts.create({
properties: { title: "Blog Post" },
body: `
# Introduction
This paragraph has **bold** and *italic* text.
## Features
- Feature A
- Feature B
\`\`\`typescript
const hello = "world"
\`\`\`
`,
})import { NotionMarkdown } from "@interactive-inc/notion-client"
const markdown = new NotionMarkdown({
heading_1: "heading_2",
heading_2: "heading_3",
})
// Create a new instance with different mapping
const updated = markdown.withMapping({
heading_1: "heading_3",
})
const posts = new NotionTable({
client,
dataSourceId: "database-id",
properties: { title: { type: "title" } } as const,
markdown,
})Use .safe to get T | Error instead of exceptions:
const result = await tasks.safe.findById("page-id")
if (result instanceof Error) {
console.error("Failed:", result.message)
} else {
console.log(result.properties())
}All methods are available on .safe: findMany, findOne, findById, create, createMany, update, updateMany, upsert, delete, deleteMany, restore.
API calls automatically retry on rate limits (429) and server errors (5xx) with exponential backoff:
const tasks = new NotionTable({
client,
dataSourceId: "database-id",
properties: { title: { type: "title" } } as const,
retry: {
maxRetries: 5,
baseDelayMs: 500,
},
})Default: 3 retries, 400ms base delay.
Reduce API calls with the built-in memory cache:
import { NotionMemoryCache } from "@interactive-inc/notion-client"
const cache = new NotionMemoryCache({
ttlMs: 60_000, // Expire entries after 60s (default: no expiration)
maxEntries: 1000, // Evict oldest entries beyond this count (default: unlimited)
})
const tasks = new NotionTable({
client,
dataSourceId: "database-id",
properties: { title: { type: "title" } } as const,
cache,
})
tasks.clearCache()Validate schema definitions at runtime using the separate models entry point:
import { zNotionPropertyConfig } from "@interactive-inc/notion-client/models"
const result = zNotionPropertyConfig.safeParse(untrusted)MIT