Skip to main content
rule

notion-api — Cursor rule: typed Notion API integrations

A Cursor rule that shapes how your agent writes Notion API code: separate modules, tokens from environment variables, official SDKs, paginated queries.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/notion-api.mdc
Open on GitHub

What notion-api does

Drop .cursor/rules/notion-api.mdc into the project and it steers everything the agent writes in the editor. The rule asks for Notion API calls to live in dedicated modules, with authentication, data fetching and transformation kept apart instead of piled into one file.

Integration tokens belong in environment variables, checked at startup, never hardcoded. For types the rule points at the official SDKs: @notionhq/client for TypeScript and JavaScript, notion-sdk for Python. Structured values go into page properties, free-form content into blocks. Database reads should carry filters and page through with start_cursor rather than pulling everything at once.

On failures it describes retries with exponential back-off for a 429 response and watching the X-RateLimit-Remaining header. If your codebase hardcodes the token or hand-builds JSON for Notion, the rule will fight your existing style on every edit — either bring the project in line or skip the file.

What it helps with

  • split Notion API calls into modules instead of one file
  • move the integration token out of source into environment variables
  • get page and database types from the official SDK
  • pull every row of a database through pagination
  • survive Notion rate limits with retries

Example prompts for the agent

  • add a function that creates a page in the projects database via the Notion API
  • rewrite the tasks database query with a filter and pagination
  • extract the Notion client into its own module and drop the hardcoded token

How to install notion-api

Manually: put the files in place

.cursor/rules/notion-api.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about notion-api

How do I install notion-api in Cursor?

Place the rule file at .cursor/rules/notion-api.mdc inside your project. Cursor picks it up as a workspace rule.

What does the notion-api rule do?

It tells the agent how to write Notion API code: modular layout, token from environment variables, official SDKs, pagination, and error handling with retries.

Which SDKs does the rule recommend?

The official ones: @notionhq/client for TypeScript and JavaScript, and notion-sdk for Python. Both give types for API parameters and responses.

Source text

notion-api.mdc · CC0-1.0
---
description: Definitive guidelines for building secure, typed, and maintainable integrations with the Notion API using modern best practices and official SDKs.
globs: **/*
---
# notion-api Best Practices

This guide establishes the definitive best practices for interacting with the Notion API. Adhering to these rules ensures our integrations are secure, performant, type-safe, and easily maintainable. We prioritize official SDKs and structured data handling.

## 1. Code Organization and Structure

Organize your Notion API interactions into dedicated modules. Separate concerns like authentication, data fetching, and data transformation.

**✅ GOOD: Modular Structure**
```typescript
// src/notion/client.ts
import { Client } from '@notionhq/client';

export const notionClient = new Client({
  auth: process.env.NOTION_API_TOKEN,
});

// src/notion/pages.ts
import { notionClient } from './client';
import { CreatePageParameters, GetPageResponse } from '@notionhq/client/build/src/api-endpoints';

export async function createNotionPage(params: CreatePageParameters): Promise<GetPageResponse> {
  return notionClient.pages.create(params);
}

// src/notion/databases.ts
import { notionClient } from './client';
import { QueryDatabaseParameters, QueryDatabaseResponse } from '@notionhq/client/build/src/api-endpoints';

export async function queryNotionDatabase(databaseId: string, params?: QueryDatabaseParameters): Promise<QueryDatabaseResponse> {
  return notionClient.databases.query({ database_id: databaseId, ...params });
}
```

**❌ BAD: Monolithic or Scattered Logic**
```typescript
// src/index.ts (or any random file)
import { Client } from '@notionhq/client';

async function main() {
  const notion = new Client({ auth: 'secret_token_hardcoded' }); // Hardcoded token is a major NO
  const response = await notion.databases.query({ database_id: 'some_id' });
  // ... page creation, block updates, all in one place
}
```

## 2. Authentication and Token Management

Always use granular integration tokens, store them in environment variables, and implement rotation. Never hardcode tokens.

**✅ GOOD: Secure Token Handling**
```typescript
// .env
NOTION_API_TOKEN="secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

// src/notion/client.ts
import { Client } from '@notionhq/client';

if (!process.env.NOTION_API_TOKEN) {
  throw new Error('NOTION_API_TOKEN is not set in environment variables.');
}

export const notionClient = new Client({
  auth: process.env.NOTION_API_TOKEN,
});
```

**❌ BAD: Insecure Token Handling**
```typescript
// Directly in code
const notion = new Client({ auth: 'secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' });
```

## 3. Typed SDKs and Data Structures

Leverage the official Notion SDKs (e.g., `@notionhq/client` for TypeScript/JavaScript, `notion-sdk` for Python) for type safety and schema validation. Prefer page properties for structured data and blocks for free-form content.

**✅ GOOD: Using Typed SDK for Page Creation**
```typescript
import { notionClient } from './client';
import { CreatePageParameters } from '@notionhq/client/build/src/api-endpoints';

async function createProjectPage(databaseId: string, projectName: string, dueDate: string) {
  const params: CreatePageParameters = {
    parent: { database_id: databaseId },
    properties: {
      'Name': {
        title: [{ text: { content: projectName } }],
      },
      'Due Date': {
        date: { start: dueDate },
      },
      'Status': {
        select: { name: 'To Do' },
      },
    },
    children: [ // Use children for rich content, not properties
      {
        object: 'block',
        type: 'paragraph',
        paragraph: {
          rich_text: [{ type: 'text', text: { content: 'Initial project description.' } }],
        },
      },
    ],
  };
  return notionClient.pages.create(params);
}
```

**❌ BAD: Manual JSON Construction & Mixing Concerns**
```typescript
// Prone to typos, schema mismatches, and difficult to maintain
async function createUntypedPage(databaseId: string, projectName: string) {
  return fetch('https://api.notion.com/v1/pages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.NOTION_API_TOKEN}`,
      'Notion-Version': '2022-06-28', // Old version, bad practice
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      parent: { database_id: databaseId },
      properties: {
        'Name': {
          title: [{ text: { content: projectName } }],
        },
        'Description': { // Attempting to put rich text in a property that expects a different type
          rich_text: [{ text: { content: 'This is a description.' } }],
        },
      },
    }),
  });
}
```

## 4. Performance Considerations

Optimize API calls by fetching only necessary data, using pagination, and batching operations where supported.

**✅ GOOD: Paginated Database Query with Filters**
```typescript
import { notionClient } from './client';
import { QueryDatabaseResponse } from '@notionhq/client/build/src/api-endpoints';

async function getAllActiveTasks(databaseId: string): Promise<QueryDatabaseResponse['results']> {
  let allResults: QueryDatabaseResponse['results'] = [];
  let cursor: string | undefined = undefined;

  while (true) {
    const response = await notionClient.databases.query({
      database_id: databaseId,
      filter: {
        property: 'Status',
        select: {
          does_not_equal: 'Done',
        },
      },
      page_size: 100, // Fetch in reasonable chunks
      start_cursor: cursor,
    });

    allResults = allResults.concat(response.results);
    if (!response.has_more) {
      break;
    }
    cursor = response.next_cursor || undefined;
  }
  return allResults;
}
```

**❌ BAD: Fetching All Data Without Pagination**
```typescript
// Will fail for large databases, or be very slow
async function getTooManyTasks(databaseId: string) {
  return notionClient.databases.query({
    database_id: databaseId,
    // No pagination, no filters
  });
}
```

## 5. Error Handling and Rate Limiting

Implement robust error handling, including retries with exponential back-off for rate limits and transient errors. Always check `X-RateLimit-Remaining`.

**✅ GOOD: Robust Error Handling with Exponential Back-off**
```typescript
import { notionClient } from './client';
import { APIResponseError } from '@notionhq/client';

async function safeNotionCall<T>(
  fn: () => Promise<T>,
  retries = 3,
  delay = 1000 // 1 second
): Promise<T> {
  try {
    return await fn();
  } catch (error) {
    if (error instanceof APIResponseError) {
      if (error.status === 429 && retries > 0) { // Rate limit error
        console.warn(`Rate limit hit. Retrying in ${delay / 1000}s...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        return safeNotionCall(fn, retries - 1, delay * 2); // Exponential back-off
      }
      console.error(`Notion API Error (${error.status}): ${error.message}`);
      throw error;
    }
    console.error('Unexpected error during Notion API call:', error);
    throw error;
  }
}

// Usage
async function updatePageSafely(pageId: string, properties: any) {
  await safeNotionCall(() =>
    notionClient.pages.update({ page_id: pageId, properties })
  );
}
```

**❌ BAD: Ignoring Errors or Blind Retries**
```typescript
async function unsafeNotionCall(pageId: string, properties: any) {
  try {
    await notionClient.pages.update({ page_id: pageId, properties });
  } catch (error) {
    // Just logs, no retry, no specific handling
    console.error('Failed to update page:', error);
  }
}
```

## 6. Request/Response Patterns

Understand and leverage the `object` and `type` fields in Notion API responses for dynamic content handling. Always assume nested blocks and rich text.

**✅ GOOD: Processing Block Children**
```typescript
import { notionClient } from './client';
import { BlockObjectResponse } from '@notionhq/client/build/src/api-endpoints';

async function processBlockChildren(blockId: string) {
  const { results } = await notionClient.blocks.children.list({ block_id: blockId });

  for (const block of results) {
    if ('type' in block) { // Type guard for BlockObjectResponse
      console.log(`Block Type: ${block.type}`);
      if (block.type === 'paragraph' && block.paragraph.rich_text) {
        console.log('Paragraph content:', block.paragraph.rich_text.map(rt => rt.plain_text).join(''));
      }
      if (block.has_children) {
        console.log(`Block ${block.id} has children. Recursively processing...`);
        await processBlockChildren(block.id); // Recursive call for nested blocks
      }
    }
  }
}
```

**❌ BAD: Assuming Flat Structure or Ignoring `has_children`**
```typescript
async function incompleteBlockProcessing(blockId: string) {
  const { results } = await notionClient.blocks.children.list({ block_id: blockId });
  // Only processes top-level blocks, misses all nested content
  for (const block of results) {
    if ('type' in block && block.type === 'paragraph') {
      console.log(block.paragraph.rich_text[0]?.plain_text);
    }
  }
}
```

## 7. Testing Approaches

Implement unit tests for utility functions and integration tests for API interactions. Use mock clients for unit tests and a dedicated, isolated Notion workspace for integration tests.

**✅ GOOD: Mocking Notion Client for Unit Tests**
```typescript
// src/utils/notionHelpers.ts
export function extractPageTitle(page: any): string {
  const titleProperty = page.properties.Name?.title;
  return titleProperty ? titleProperty.map((t: any) => t.plain_text).join('') : 'Untitled';
}

// tests/notionHelpers.test.ts
import { extractPageTitle } from '../src/utils/notionHelpers';

describe('extractPageTitle', () => {
  it('should extract the title from a Notion page object', () => {
    const mockPage = {
      properties: {
        Name: {
          title: [{ type: 'text', text: { content: 'My Test Page' }, plain_text: 'My Test Page' }],
        },
      },
    };
    expect(extractPageTitle(mockPage)).toBe('My Test Page');
  });

  it('should return "Untitled" if title property is missing', () => {
    const mockPage = { properties: {} };
    expect(extractPageTitle(mockPage)).toBe('Untitled');
  });
});
```

**✅ GOOD: Integration Tests with Dedicated Workspace**
*   Set up a separate Notion workspace or database specifically for testing.
*   Use a dedicated integration token with minimal permissions for this test workspace.
*   Clean up test data after each test run.

```typescript
// tests/integration/notion.test.ts
import { notionClient } from '../../src/notion/client'; // Your actual client
import { v4 as uuidv4 } from 'uuid';

const TEST_DATABASE_ID = process.env.NOTION_TEST_DATABASE_ID!; // Ensure this is set

describe('Notion API Integration', () => {
  let createdPageId: string;

  beforeAll(() => {
    if (!TEST_DATABASE_ID) {
      throw new Error('NOTION_TEST_DATABASE_ID must be set for integration tests.');
    }
  });

  afterEach(async () => {
    // Clean up created pages
    if (createdPageId) {
      await notionClient.pages.update({
        page_id: createdPageId,
        archived: true, // Archive instead of delete to preserve history if needed
      });
      createdPageId = ''; // Reset for next test
    }
  });

  it('should create and retrieve a page in the test database', async () => {
    const pageTitle = `Test Page ${uuidv4()}`;
    const newPage = await notionClient.pages.create({
      parent: { database_id: TEST_DATABASE_ID },
      properties: {
        'Name': {
          title: [{ text: { content: pageTitle } }],
        },
      },
    });
    createdPageId = newPage.id;
    expect(newPage.properties.Name.type).toBe('title');

    const retrievedPage = await notionClient.pages.retrieve({ page_id: createdPageId });
    expect(extractPageTitle(retrievedPage)).toBe(pageTitle);
  });
});
```

**❌ BAD: No Testing or Manual Testing Only**
*   Relying solely on manual checks after deployment.
*   No automated verification of API interactions.

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Backend and APIs →

From the same set69

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor