@sug1t0m0/notion-typed-client
v6.0.0
Published
Type-safe wrapper for Notion SDK with database schema injection
Downloads
1,036
Maintainers
Readme
notion-typed-client
Type-safe code generation tool for Notion API clients
⚠️ Early Stage Notice: E2E testing revealed that this library still has bugs. Thank you to everyone who has downloaded and tried it - your interest means a lot! My sincere apologies for any issues you've encountered. I'm committed to improving this library until it's production-ready for my own use cases and yours.
Overview
notion-typed-client solves the type-safety issues of generic Notion API clients. It fetches database schemas from Notion and automatically generates TypeScript type definitions and type-safe API clients with validation capabilities.
Key Features
- 🔒 Complete Type Safety - Represents actual options for select/multi_select/status properties as types
- 🔄 Automatic Schema Sync - Fetches latest schema information from Notion API and generates types
- ✅ Runtime Validation - Runtime value validation with AJV
- 🎯 Workflow-Specific - Generates clients optimized for specific database structures
- 🔧 Auto-Configuration Updates - Automatic ID resolution and notionName change detection
- 🔌 Dependency Injection - Inject custom Notion client implementations for testing and customization
Installation
npm install -D @sug1t0m0/notion-typed-client
# or
pnpm add -D @sug1t0m0/notion-typed-client
# or
yarn add -D @sug1t0m0/notion-typed-clientQuick Start
1. Initial Setup
npx notion-typed-client initSet your Notion API key as an environment variable or in your project's .env file:
NOTION_API_KEY=your_notion_api_key_hereNote: For examples and testing, see the
/exampleand/e2edirectories which have their own.env.examplefiles.
2. Create Configuration File
notion-typed.config.ts:
import { NotionTypedConfig } from 'notion-typed-client';
const config: NotionTypedConfig = {
databases: [
{
id: null, // OK to be null initially, will be auto-resolved later
name: 'TaskDatabase',
displayName: 'Task Database',
notionName: 'Tasks',
properties: [
{
id: null,
name: 'title',
displayName: 'Title',
notionName: 'Title',
type: 'title'
},
{
id: null,
name: 'status',
displayName: 'Status',
notionName: 'Status',
type: 'status'
// options will be fetched automatically
},
{
id: null,
name: 'priority',
displayName: 'Priority',
notionName: 'Priority',
type: 'select'
// options will be fetched automatically
},
{
id: null,
name: 'tags',
displayName: 'Tags',
notionName: 'Tags',
type: 'multi_select'
// options will be fetched automatically
},
{
id: null,
name: 'customField',
displayName: 'Custom Field',
notionName: 'Custom Field',
type: null // type will also be auto-detected from Notion
}
]
}
],
output: {
path: './src/generated',
clientName: 'NotionClient'
}
};
export default config;3. Fetch Schema and Generate Client
# Fetch schema, resolve IDs, and generate types in one command
npx notion-typed-client build
# Or run individually
npx notion-typed-client fetch # Fetch schema and resolve IDs
npx notion-typed-client generate # Generate types and client4. Use the Generated Client
Basic Usage
import { NotionTypedClient } from './generated/client';
import { TaskDatabase } from './generated/types';
import { Client } from '@notionhq/client';
// Create official Notion client
const notionClient = new Client({
auth: process.env.NOTION_API_KEY!
});
// Use with generated typed client
const client = new NotionTypedClient({
client: notionClient
});
// Type-safe page creation
const newTask = await client.createPage('TaskDatabase', {
title: 'New Task', // string type
status: 'In Progress', // Only 'Not Started' | 'In Progress' | 'Done' allowed
priority: 'High', // Only 'Low' | 'Medium' | 'High' allowed
tags: ['urgent', 'review'] // Only defined options allowed
});
// Type-safe query with filter support
const tasks = await client.queryDatabase('TaskDatabase', {
filter: {
property: 'status',
status: {
equals: 'In Progress' // Type-safe: only valid status options allowed
}
}
});
// Compound filters (and/or) are also type-safe
const filteredTasks = await client.queryDatabase('TaskDatabase', {
filter: {
and: [
{ property: 'status', status: { equals: 'In Progress' } },
{ property: 'priority', select: { equals: 'High' } }
]
}
});
// Fetch all pages from a database (pagination handled automatically)
const allTasks = await client.queryDatabaseAll('TaskDatabase', {
filter: {
property: 'status',
status: { does_not_equal: 'Archived' }
},
sorts: [
{ property: 'priority', direction: 'descending' }, // Code names are auto-converted
{ property: 'dueDate', direction: 'ascending' } // to Notion property names
]
});
console.log(`Total tasks: ${allTasks.length}`);
// Memory-efficient iteration through large datasets
for await (const task of client.queryDatabaseIterator('TaskDatabase', {
filter: { property: 'status', status: { equals: 'Pending' } },
page_size: 50 // Process in smaller chunks
})) {
// Process each task individually
await processTask(task);
// Can break early if needed
if (shouldStop(task)) {
break;
}
}
// Page update with validation
const updated = await client.updatePage('page-id', 'TaskDatabase', {
status: 'Done' // Also validated at runtime
});Dependency Injection (Advanced)
You can inject your own Notion client implementation for testing or customization:
import { NotionTypedClient } from './generated/client';
import type { NotionClientInterface } from '@sug1t0m0/notion-typed-client';
// Mock client for testing
const mockClient: NotionClientInterface = {
databases: {
retrieve: jest.fn(),
query: jest.fn(),
},
pages: {
create: jest.fn(),
retrieve: jest.fn(),
update: jest.fn(),
},
search: jest.fn(),
};
// Use injected client
const client = new NotionTypedClient({
client: mockClient
});
// Your custom client with logging, retry logic, etc.
class CustomNotionClient implements NotionClientInterface {
private baseClient: Client;
constructor(options: { auth: string }) {
this.baseClient = new Client(options);
}
databases = {
retrieve: async (args) => {
console.log('Retrieving database:', args.database_id);
return this.baseClient.databases.retrieve(args);
},
query: async (args) => {
console.log('Querying database:', args.database_id);
return this.baseClient.databases.query(args);
},
};
pages = {
create: async (args) => {
console.log('Creating page');
return this.baseClient.pages.create(args);
},
retrieve: async (args) => {
console.log('Retrieving page:', args.page_id);
return this.baseClient.pages.retrieve(args);
},
update: async (args) => {
console.log('Updating page:', args.page_id);
return this.baseClient.pages.update(args);
},
};
search = async (args) => {
console.log('Searching:', args.query);
return this.baseClient.search(args);
};
}
const customClient = new CustomNotionClient({
auth: process.env.NOTION_API_KEY!
});
const client = new NotionTypedClient({
client: customClient
});Pagination Methods
The generated client includes three methods for querying databases, each optimized for different use cases:
queryDatabase - Standard pagination
Returns a single page of results with pagination metadata. Ideal for UI pagination and controlled data fetching.
const page = await client.queryDatabase('TaskDatabase', {
filter: { property: 'status', status: { equals: 'Active' } },
sorts: [{ property: 'created_time', direction: 'descending' }],
page_size: 20,
start_cursor: previousCursor // For fetching next page
});
// page.results: Array of up to 20 items
// page.has_more: boolean indicating if more pages exist
// page.next_cursor: cursor for fetching the next pagequeryDatabaseAll - Fetch all records
Automatically fetches all pages and returns them as a single array. Best for data analysis and small to medium datasets.
const allTasks = await client.queryDatabaseAll('TaskDatabase', {
filter: { property: 'status', status: { equals: 'Active' } },
sorts: [{ property: 'created_time', direction: 'descending' }],
page_size: 100 // Optional: controls internal pagination size
});
// allTasks: Array containing ALL matching records
// ⚠️ Use with caution for large datasets (memory usage)queryDatabaseIterator - Memory-efficient iteration
Returns an async iterator that fetches pages on-demand. Perfect for processing large datasets without loading everything into memory.
// Process millions of records with minimal memory usage
for await (const task of client.queryDatabaseIterator('TaskDatabase', {
filter: { property: 'archived', checkbox: { equals: false } },
page_size: 50
})) {
await sendEmailNotification(task);
// Can exit early if needed
if (await shouldStopProcessing()) {
break;
}
}When to use each method:
queryDatabase: UI pagination, manual cursor control, specific page fetchingqueryDatabaseAll: Reports, data analysis, small datasets (< 1000 items)queryDatabaseIterator: Batch processing, ETL, large datasets, memory-constrained environments
CLI Commands
init
Creates configuration file template and necessary files.
npx notion-typed-client init [options]
Options:
--config <path> Configuration file path (default: "./notion-typed.config.ts")
--force Overwrite existing filesfetch
Fetches schema information from Notion API, resolves IDs, and updates configuration file.
npx notion-typed-client fetch [options]
Options:
--config <path> Configuration file path
--dry-run Preview changes without applying themgenerate
Generates type definitions and client code from fetched schema.
npx notion-typed-client generate [options]
Options:
--config <path> Configuration file path
--watch Watch for file changes and auto-generatebuild
Runs fetch and generate sequentially.
npx notion-typed-client build [options]validate
Validates consistency between configuration and actual Notion schema.
npx notion-typed-client validate [options]Configuration File Details
DatabaseConfig
| Property | Type | Description |
|-----------|-----|------|
| id | string \| null | Database ID (searches by notionName if null) |
| name | string | Type name in TypeScript |
| displayName | string | Display name for logs |
| notionName | string | Actual name in Notion |
| properties | PropertyConfig[] | Array of property configurations |
PropertyConfig
| Property | Type | Description |
|-----------|-----|------|
| id | string \| null | Property ID (searches by notionName if null) |
| name | string | Property name in TypeScript |
| displayName | string | Display name for logs |
| notionName | string | Actual property name in Notion |
| type | NotionPropertyType \| null | Property type (auto-detected from Notion if null) |
Supported Property Types
title- Titlerich_text- Rich Textnumber- Numberselect- Single Select (options auto-fetched)multi_select- Multi Select (options auto-fetched)status- Status (options and groups auto-fetched)date- Datepeople- Peoplefiles- Filescheckbox- Checkboxurl- URLemail- Emailphone_number- Phone Numberformula- Formularelation- Relationrollup- Rollupcreated_time- Created Timecreated_by- Created Bylast_edited_time- Last Edited Timelast_edited_by- Last Edited By
Auto-Update Features
ID Resolution
On first run, entries with id: null are automatically resolved using notionName and the configuration file is updated.
Name Change Detection
When IDs already exist, detects name changes in Notion and prompts to update notionName in the configuration file.
Auto-Fetching Options
Options for select/multi_select/status properties are automatically fetched from Notion API and reflected in type definitions. No manual management needed.
Generated Files
By default, the following files are generated in the ./notion-typed-codegen/ directory:
types.ts- TypeScript type definitionsclient.ts- Type-safe Notion clientvalidators.ts- Runtime validatorsschemas.json- JSON Schema definitions
Frequently Asked Questions (FAQ)
Q: Why do I need this tool?
A: The official Notion SDK is generic and doesn't provide database-specific type information. This tool generates type-safe clients based on your database structure, significantly improving the development experience.
Q: What happens when options are changed?
A: Running npx notion-typed-client build fetches the latest schema and updates type definitions. Added, removed, or changed options are automatically reflected.
Q: Can I manage multiple databases?
A: Yes, you can add multiple database configurations to the databases array in notion-typed.config.ts.
Q: Can I customize the generated code?
A: Generated code is overwritten, so don't edit it directly. If customization is needed, create your own class that wraps the generated client.
Best Practices
1. Environment Variable Management
# .env.local (development)
NOTION_API_KEY=secret_development_key
# .env.production (production)
NOTION_API_KEY=secret_production_key2. CI/CD Usage
# GitHub Actions example
- name: Generate Notion types
env:
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
run: |
npx notion-typed-client build
npx tsc --noEmit3. Type Reuse
// Use generated types in other files
import type { PlansDatabase } from './notion-typed-codegen/types';
function processPlans(plans: PlansDatabase[]): void {
// Type-safe processing
}Troubleshooting
Invalid API Key
Verify that the correct Notion API key is set in your environment variables or project's .env file.
Database Not Found
- Check that your Notion API key has access permissions to the database
- Verify that
notionNamematches exactly (case-sensitive)
Type Errors
Run npx notion-typed-client build to regenerate types with the latest schema.
Release Management
This project follows Semantic Versioning.
Automatic Releases
- feat: prefix commits trigger minor version bumps
- fix: prefix commits trigger patch version bumps
- BREAKING CHANGE: or commits with
!trigger major version bumps
Manual Releases
Run the "Release" workflow from the GitHub Actions tab to select version type.
Version Management
- MAJOR (x.0.0): Breaking changes
- MINOR (1.x.0): New features (backward compatible)
- PATCH (1.1.x): Bug fixes
Conventional Commits
Commit messages should follow this format:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Examples:
feat(cli): add new init command
fix: resolve type generation bug
docs: update README with examples
BREAKING CHANGE: remove deprecated APITo use the configured commit message template:
git config commit.template .gitmessageLicense
MIT
Contributing
Issues and Pull Requests are welcome!
