Notion как база данных для сайта: API, примеры кода и ограничения

Обновлено:

Notion можно использовать как базу данных для сайта — через официальный Notion API с полноценным CRUD. Типичный сценарий: контент-менеджер редактирует данные в привычном интерфейсе Notion, а сайт получает их через API (паттерн headless CMS). Ниже — как это реализовать, где граница разумного и как не потерять контент, который живёт только в чужом облаке.


Что такое Notion Database

«База данных» в Notion — таблица, строки которой являются страницами. Каждая строка — одновременно запись и полноценная страница с контентом. Свойства (Properties) — аналог колонок в SQL.

Типы свойств:

  • Title — обязательный заголовок;
  • Text, Number, Date, Checkbox — базовые типы;
  • Select, Multi-select — списки значений;
  • Relation — связь с другой базой (аналог внешнего ключа);
  • Formula, Rollup — вычисляемые поля;
  • Files & media — файлы;
  • Person — участники воркспейса;
  • URL, Email, Phone — специализированные текстовые поля.

Пример структуры блога в Notion:

| Название статьи | Slug | Статус | Теги | Дата публикации | Автор | |---|---|---|---|---|---| | Что такое MongoDB | mongodb-guide | Published | MongoDB, NoSQL | 2026-03-01 | Иван | | Уязвимости Redis | redis-security | Draft | Redis, Security | — | Мария |


Notion API: что умеет и как работает

Для работы с REST API нужно:

  1. Создать интеграцию на notion.so/my-integrations.
  2. Получить токен интеграции (Internal Integration Secret).
  3. Дать интеграции доступ к нужным страницам (меню страницы → Connections).

Примеры ниже используют версию API 2022-06-28. В более новых версиях Notion разделил базы данных и источники данных (data sources), и эндпоинты запросов изменились — при обновлении SDK сверяйтесь с документацией и всегда явно фиксируйте версию.

Базовые запросы

# Записи из базы с фильтром и сортировкой
curl -X POST "https://api.notion.com/v1/databases/$DATABASE_ID/query" \
  -H "Authorization: Bearer $NOTION_TOKEN" \
  -H "Notion-Version: 2022-06-28" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "property": "Статус",
      "select": { "equals": "Published" }
    },
    "sorts": [
      { "property": "Дата публикации", "direction": "descending" }
    ]
  }'
# Содержимое страницы (блоки)
curl "https://api.notion.com/v1/blocks/$PAGE_ID/children" \
  -H "Authorization: Bearer $NOTION_TOKEN" \
  -H "Notion-Version: 2022-06-28"

Реализация: Notion как CMS для блога на Next.js

Установка

npm install @notionhq/client notion-to-md react-markdown

Клиент и запросы к API

// lib/notion.ts
import { Client } from '@notionhq/client'
import { NotionToMarkdown } from 'notion-to-md'

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
  notionVersion: '2022-06-28',
})

const n2m = new NotionToMarkdown({ notionClient: notion })

const DATABASE_ID = process.env.NOTION_DATABASE_ID!

export async function getAllPosts() {
  const response = await notion.databases.query({
    database_id: DATABASE_ID,
    filter: {
      property: 'Статус',
      select: { equals: 'Published' }
    },
    sorts: [
      { property: 'Дата публикации', direction: 'descending' }
    ]
  })

  return response.results.map((page: any) => ({
    id: page.id,
    title: page.properties['Название статьи'].title[0]?.plain_text || '',
    slug: page.properties['Slug'].rich_text[0]?.plain_text || '',
    date: page.properties['Дата публикации'].date?.start || null,
    tags: page.properties['Теги'].multi_select.map((t: any) => t.name),
    author: page.properties['Автор'].people[0]?.name || ''
  }))
}

export async function getPostBySlug(slug: string) {
  const response = await notion.databases.query({
    database_id: DATABASE_ID,
    filter: {
      property: 'Slug',
      rich_text: { equals: slug }
    }
  })

  if (!response.results.length) return null

  const page = response.results[0] as any
  const mdBlocks = await n2m.pageToMarkdown(page.id)
  const markdown = n2m.toMarkdownString(mdBlocks)

  return {
    id: page.id,
    title: page.properties['Название статьи'].title[0]?.plain_text || '',
    content: markdown.parent,
    date: page.properties['Дата публикации'].date?.start || null,
  }
}

Страница списка статей

// app/blog/page.tsx
import { getAllPosts } from '@/lib/notion'

export default async function BlogPage() {
  const posts = await getAllPosts()

  return (
    <div>
      <h1>Блог</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
          <p>{post.date}</p>
          <div>{post.tags.map((tag: string) => <span key={tag}>{tag}</span>)}</div>
        </article>
      ))}
    </div>
  )
}

Страница статьи с рендерингом Markdown

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import { getPostBySlug, getAllPosts } from '@/lib/notion'

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPostBySlug(slug)
  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <ReactMarkdown>{post.content}</ReactMarkdown>
    </article>
  )
}

Реализация на Python (Flask/FastAPI)

pip install notion-client
import os
from notion_client import Client

notion = Client(auth=os.environ["NOTION_TOKEN"], notion_version="2022-06-28")
DATABASE_ID = os.environ["NOTION_DATABASE_ID"]

def get_published_posts():
    response = notion.databases.query(
        database_id=DATABASE_ID,
        filter={"property": "Статус", "select": {"equals": "Published"}},
        sorts=[{"property": "Дата публикации", "direction": "descending"}],
    )

    posts = []
    for page in response["results"]:
        props = page["properties"]
        posts.append({
            "id": page["id"],
            "title": props["Название статьи"]["title"][0]["plain_text"]
                     if props["Название статьи"]["title"] else "",
            "slug": props["Slug"]["rich_text"][0]["plain_text"]
                    if props["Slug"]["rich_text"] else "",
            "date": props["Дата публикации"]["date"]["start"]
                    if props["Дата публикации"]["date"] else None,
        })
    return posts

def create_post(title: str, slug: str, content: str):
    notion.pages.create(
        parent={"database_id": DATABASE_ID},
        properties={
            "Название статьи": {"title": [{"text": {"content": title}}]},
            "Slug": {"rich_text": [{"text": {"content": slug}}]},
            "Статус": {"select": {"name": "Draft"}},
        },
        children=[{
            "object": "block",
            "type": "paragraph",
            "paragraph": {
                "rich_text": [{"type": "text", "text": {"content": content}}]
            }
        }]
    )

Кэширование: обязательно для продакшена

Notion API ограничивает частоту запросов — в среднем около 3 запросов в секунду на интеграцию, при превышении возвращается 429. При трафике без кэша сайт упрётся в лимит.

Кэширование в Next.js

// Кэш на 1 час
export async function getAllPostsRaw() {
  const response = await fetch(
    `https://api.notion.com/v1/databases/${process.env.NOTION_DATABASE_ID}/query`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.NOTION_TOKEN}`,
        'Notion-Version': '2022-06-28',
        'Content-Type': 'application/json'
      },
      next: { revalidate: 3600 }
    }
  )
  return response.json()
}

Кэширование через Redis

import json
import redis

r = redis.Redis(host="localhost", port=6379, db=0)

def get_posts_cached():
    cached = r.get("notion:posts")
    if cached:
        return json.loads(cached)

    posts = get_published_posts()
    r.setex("notion:posts", 3600, json.dumps(posts, ensure_ascii=False))
    return posts

Инвалидация кэша по событию

Чтобы сайт обновлялся сразу после правки в Notion, подпишите свой эндпоинт на вебхуки интеграции Notion или используйте автоматизации базы с отправкой вебхука (на платных тарифах). Альтернатива — Zapier или Make: триггер «страница обновлена» → HTTP-запрос на ваш эндпоинт очистки кэша.


Ограничения Notion как базы данных

| Ограничение | Что означает | |---|---| | Rate limit ~3 req/s | Нужен кэш для трафика | | До 100 записей за запрос | Большие базы читаются постранично, медленно | | Нет транзакций | Нельзя атомарно обновить несколько записей | | Ограниченный поиск через API | Полнотекстовый поиск нужно делать у себя | | Файлы по временным ссылкам | Ссылки на загруженные файлы истекают примерно через час | | Зависимость от Notion | Если Notion недоступен и нет кэша — сайт не работает | | Данные за рубежом | Персональные данные россиян по 152-ФЗ хранить нельзя |


Бэкап и экспорт Notion

Весь контент — свойства записей, тексты страниц, файлы — хранится только в облаке Notion. История версий страниц и корзина помогают отменить ошибку, но глубина истории зависит от тарифа, а при потере доступа к аккаунту или удалении базы через API не поможет ничего, кроме внешней копии.

Ручной экспорт. Меню страницы или базы → Export → «Markdown & CSV» (с подстраницами): база выгружается в CSV, страницы — в .md. Владелец воркспейса может экспортировать всё через настройки воркспейса. Это удобно разово, но не по расписанию.

Выгрузка через API. Скрипт по cron забирает все записи (пагинацию берёт на себя хелпер SDK) и складывает их в JSON и SQLite:

# notion_dump.py
import datetime, json, os, sqlite3
from notion_client import Client
from notion_client.helpers import collect_paginated_api

notion = Client(auth=os.environ["NOTION_TOKEN"], notion_version="2022-06-28")
stamp = datetime.date.today().isoformat()
pages = collect_paginated_api(notion.databases.query, database_id=os.environ["NOTION_DATABASE_ID"])

with open(f"notion-{stamp}.json", "w", encoding="utf-8") as f:
    json.dump(pages, f, ensure_ascii=False, indent=2)

db = sqlite3.connect(f"notion-{stamp}.db")
db.execute("CREATE TABLE pages (id TEXT PRIMARY KEY, edited TEXT, properties TEXT)")
db.executemany("INSERT INTO pages VALUES (?, ?, ?)",
               [(p["id"], p["last_edited_time"], json.dumps(p["properties"], ensure_ascii=False)) for p in pages])
db.commit()
db.close()

Если важен текст страниц, добавьте выгрузку блоков в Markdown через notion-to-md, а файлы скачивайте сразу — ссылки истекают. Готовую копию отправляйте за пределы Notion — в S3 или в dbsend, где у каждой версии видно число записей и diff с предыдущей:

python notion_dump.py
dbsend backup "notion-$(date +%F).db" -d "$DBSEND_DATABASE_ID" -e sqlite -l "notion-$(date +%F)"

Команды CLI — в документации, про хранение JSON в SQLite — в статье JSON как база данных. Раз в квартал проверяйте копию: импортируйте CSV в пустую тестовую страницу или восстановите несколько записей через pages.create — см. как проверить, что бэкап рабочий.

Когда переезжать на SQLite или PostgreSQL: контент стал данными приложения (заказы, пользователи), нужны транзакции и сложные запросы, упираетесь в rate limit или 152-ФЗ. Тот же дамп — отправная точка миграции; похожий подход для Airtable описан в статье Airtable как база данных.


Когда Notion как БД — хороший выбор

  • блог или контент-сайт с умеренным числом материалов;
  • портфолио с редко меняющимся контентом;
  • внутренние инструменты команды (трекеры, дашборды);
  • MVP, где главное — скорость запуска;
  • продукт, где нетехнические люди редактируют контент без отдельной CMS;
  • лендинги с динамическими блоками (FAQ, отзывы, команда).

Когда Notion — плохой выбор

  • высоконагруженные сайты без серьёзного кэширования;
  • транзакционные системы (заказы, платежи);
  • приложения с пользовательскими и персональными данными;
  • сложные поисковые сценарии;
  • данные, которые должны жить долго без зависимости от SaaS.

Инструменты для работы с Notion как БД

  • @notionhq/client — официальный SDK для JavaScript/TypeScript;
  • notion-client — Python SDK;
  • notion-to-md — конвертация страниц в Markdown;
  • react-notion-x — рендеринг публичных страниц Notion в React;
  • Super.so, Feather, Notaku — сайты и блоги поверх Notion без кода.

Notion для внутренних инструментов

Notion хорошо подходит для внутренних задач команды: трекер с дополнительными полями, простая CRM, редакционный план, база знаний, дашборд проекта с rollup-вычислениями.

Через API их можно автоматизировать:

# Создать задачу при новом заказе
def create_task_from_order(order):
    notion.pages.create(
        parent={"database_id": TASKS_DB_ID},
        properties={
            "Название": {"title": [{"text": {"content": f"Обработать заказ #{order['id']}"}}]},
            "Приоритет": {"select": {"name": "Высокий"}},
            "Дедлайн": {"date": {"start": order["deadline"]}},
            "Статус": {"select": {"name": "Новая"}},
        }
    )

Частые вопросы

Можно ли использовать Notion как основной backend для продакшн-приложения?

Для контентных сайтов — да, с кэшированием. Для приложений с большим числом пользователей, транзакциями или персональными данными — нет: используйте PostgreSQL или другую полноценную БД.

Как часто Notion меняет API?

Нечасто, но изменения бывают, в том числе ломающие. Всегда указывайте версию в заголовке Notion-Version и следите за changelog на developers.notion.com.

Бесплатен ли Notion API?

API доступен на всех планах, включая бесплатный. Ограничения бесплатного плана касаются в основном участников, загрузки файлов и глубины истории версий.

Что лучше для CMS: Notion или Contentful?

Contentful и Sanity — специализированные headless CMS с предсказуемым API, CDN и вебхуками. Notion удобнее команде, которая уже в нём работает. Для серьёзного контент-проекта — специализированная CMS, для MVP или внутреннего инструмента — Notion.

Как защитить токен Notion?

Не хардкодьте токен, храните его в переменных окружения. Не вызывайте Notion API из браузера — токен увидит любой посетитель. Давайте интеграции доступ только к тем страницам, которые ей нужны.

Как сделать бэкап базы Notion автоматически?

Встроенного экспорта по расписанию нет. Запускайте по cron скрипт, который выгружает записи через API в JSON или SQLite, скачивает файлы и отправляет копию во внешнее хранилище. Пример — в разделе «Бэкап и экспорт Notion» выше.