A TypeScript library that transforms Notion into a powerful, type-safe database. Seamlessly convert between Notion blocks and markdown, while enjoying simple database operations without the complexity of Notion's API.
https://interactive-inc.github.io/open-notion-client/
Working directly with Notion's API can be overwhelming. The API responses include extensive formatting information like text colors, bold/italic styles, annotations, and deeply nested metadata structures. What should be a simple database query becomes a complex parsing exercise.
For example, retrieving a simple text value requires navigating through multiple nested objects:
{
"properties": {
"Name": {
"id": "title",
"type": "title",
"title": [{
"type": "text",
"text": { "content": "Hello World" },
"annotations": { "bold": false, "italic": false, "color": "default" },
"plain_text": "Hello World"
}]
}
}
}notion-client simplifies this to just { name: "Hello World" }, making Notion as easy to use as any traditional database.
- Bidirectional Conversion: Convert between Notion blocks and markdown text
- Type-safe Database Operations: Strongly-typed CRUD operations for Notion databases
- Block Type Support: Supports paragraphs, headings (H1-H3), lists, and code blocks
- Rich Text Formatting: Handles bold, italic, strikethrough, and inline code
- Recursive Block Fetching: Automatically fetches nested child blocks
- Advanced Querying: Filter, sort, and paginate database queries
bun i @interactive-inc/notion-clientimport { Client } from '@notionhq/client'
import { NotionTable } from '@interactive-inc/notion-client'
const client = new Client({ auth: process.env.NOTION_TOKEN })
const tasksTable = new NotionTable({
client,
tableId: 'your-database-id',
schema: {
title: { type: 'title' },
status: { type: 'select', options: ['todo', 'in_progress', 'done'] },
priority: { type: 'number' },
tags: { type: 'multi_select', options: ['bug', 'feature', 'enhancement'] }
} as const
})
// Create a record
const task = await tasksTable.create({
title: 'Implement new feature',
status: 'todo',
priority: 1,
tags: ['feature']
})
// Query records with filtering and sorting
const { records } = await tasksTable.findMany({
where: {
status: 'in_progress',
priority: { $gte: 3 }
},
sorts: [{ property: 'priority', direction: 'descending' }],
limit: 10
})
// Update a record
await tasksTable.update(task.id, {
status: 'done'
})import { NotionTable, NotionMarkdown } from '@interactive-inc/notion-client'
// Create a markdown enhancer to transform heading levels
const enhancer = new NotionMarkdown({
heading_1: 'heading_2', // Convert H1 to H2
heading_2: 'heading_3' // Convert H2 to H3
})
// Create table with markdown support
const blogTable = new NotionTable({
client,
tableId: 'your-blog-database-id',
schema: {
title: { type: 'title' },
content: { type: 'rich_text' }
},
enhancer // Optional: transforms markdown before saving
})
// Create a post with markdown content
const post = await blogTable.create({
title: 'My Blog Post',
body: `# Introduction
This is a paragraph with **bold** and *italic* text.
## Features
- Feature 1
- Feature 2
\`\`\`typescript
const hello = "world"
\`\`\`
`
})// Complex queries with operators
const results = await tasksTable.findMany({
where: {
$or: [
{ status: 'todo' },
{
$and: [
{ priority: { $gte: 5 } },
{ tags: { $contains: 'urgent' } }
]
}
]
},
sorts: [
{ property: 'priority', direction: 'descending' },
{ property: 'created_time', direction: 'ascending' }
],
limit: 20
})
// Find one record
const urgentTask = await tasksTable.findOne({
where: {
status: 'todo',
priority: { $gte: 8 }
}
})
// Update multiple records
await tasksTable.updateMany({
where: { status: 'todo' },
data: { status: 'in_progress' }
})const userTable = new NotionTable({
client,
tableId: 'users-database-id',
schema: {
email: {
type: 'email',
validate: (value) => {
if (!value.includes('@')) {
return 'Invalid email format'
}
return true
}
},
age: {
type: 'number',
min: 0,
max: 120
}
} as const,
hooks: {
beforeCreate: async (data) => {
// Add timestamp
return {
...data,
created_at: new Date().toISOString()
}
},
afterFind: async (records) => {
// Transform data after fetching
return records.map(record => ({
...record,
displayName: record.email.split('@')[0]
}))
}
}
})| Markdown | Notion Block Type | Example |
|---|---|---|
| Plain text | paragraph |
Hello world |
# Heading 1 |
heading_1 |
# Title |
## Heading 2 |
heading_2 |
## Subtitle |
### Heading 3 |
heading_3 |
### Section |
- Item |
bulleted_list_item |
- List item |
1. Item |
numbered_list_item |
1. First item |
```code``` |
code |
js<br>console.log()<br> |
**bold** |
Rich text with bold | Important |
*italic* |
Rich text with italic | Emphasis |
~~strike~~ |
Rich text with strikethrough | |
`code` |
Rich text with code | variable |
- Paragraph blocks → Plain text
- Heading 1, 2, 3 blocks →
#,##,### - Bulleted list items →
-lists - Numbered list items →
1.lists - Code blocks →
```fenced code blocks - Rich text formatting preserved (bold, italic, strikethrough, inline code)
- Plain text → Paragraph blocks
- Headers (
#,##,###) → Heading blocks - Unordered lists (
-,*,+) → Bulleted list items - Ordered lists (
1.,2.) → Numbered list items - Fenced code blocks → Code blocks with language support
- Inline formatting → Rich text with annotations
Create a type-safe client for Notion database operations.
new NotionTable({
client: Client, // Notion API client
tableId: string, // Database ID
schema: Schema, // Database schema definition
enhancer?: NotionMarkdown, // Optional markdown transformer
hooks?: { // Optional lifecycle hooks
beforeCreate?: (data) => Promise<data>
afterCreate?: (record) => Promise<record>
beforeUpdate?: (id, data) => Promise<data>
afterUpdate?: (record) => Promise<record>
beforeFind?: (options) => Promise<options>
afterFind?: (records) => Promise<records>
}
})Transform markdown content when saving to Notion.
new NotionMarkdown({
heading_1?: 'heading_1' | 'heading_2' | 'heading_3',
heading_2?: 'heading_1' | 'heading_2' | 'heading_3',
heading_3?: 'heading_1' | 'heading_2' | 'heading_3'
})-
findMany(options?)- Query multiple recordswhere- Filter conditions with operators ($eq, $ne, $gt, $gte, $lt, $lte, $contains, $or, $and, $not)sorts- Array of sort specificationslimit- Maximum number of recordscursor- Pagination cursor
-
findOne(options?)- Find the first matching record -
findById(id: string)- Get a record by ID -
create(data)- Create a new record with optional markdown body -
update(id, data)- Update a record -
updateMany(options)- Update multiple records -
upsert(options)- Create or update based on conditions -
delete(id)- Archive a record -
deleteMany(where?)- Archive multiple records -
restore(id)- Restore an archived record
title- Page title (required for all databases)rich_text- Plain text contentnumber- Numeric values with optional min/max validationselect- Single selection from predefined optionsmulti_select- Multiple selections from predefined optionscheckbox- Boolean valuesurl- URL stringsemail- Email addressesphone_number- Phone numbersdate- Date valuesfiles- File attachmentspeople- User referencesrelation- Relations to other databasesformula- Computed valuesrollup- Aggregated values from relations