База данных SQLite
Пока статьи хранятся в массиве, они пропадают при каждом перезапуске сервера. Нужна база данных. Для учебного блога удобна SQLite: вся база — один файл в папке проекта, отдельный сервер не нужен. На Node.js с ней работает пакет better-sqlite3.
Установка
npm install better-sqlite3
npm install -D @types/better-sqlite3Второй пакет содержит типы для TypeScript и нужен только при разработке.
Подключение и таблицы
Создайте src/db.ts:
// src/db.ts
import Database from 'better-sqlite3'
export const db = new Database(process.env.DB_FILE ?? 'blog.db')
db.pragma('foreign_keys = ON')
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author TEXT NOT NULL,
text TEXT NOT NULL
);
`)
export type Post = {
id: number
title: string
body: string
created_at: string
}При первом импорте этого модуля в корне проекта появится файл blog.db с двумя таблицами. IF NOT EXISTS не даёт создать таблицы повторно при следующих запусках. Строка с pragma включает проверку связей: без неё SQLite не удалит комментарии вместе со статьёй. Добавьте blog.db в .gitignore.
Запросы
Запрос сначала готовят методом prepare(), а затем выполняют одним из трёх методов:
| Метод | Для чего | Результат |
|---|---|---|
.all(...) | SELECT нескольких строк | массив объектов |
.get(...) | SELECT одной строки | объект или undefined |
.run(...) | INSERT, UPDATE, DELETE | changes и lastInsertRowid |
Знаки ? в запросе заменяются аргументами по порядку.
Чтение статей
Используйте базу в src/routes/posts.ts:
// src/routes/posts.ts
import { Hono } from 'hono'
import { db, type Post } from '../db.js'
const posts = new Hono()
posts.get('/', (c) => {
const rows = db.prepare('SELECT * FROM posts ORDER BY id DESC').all() as Post[]
return c.json(rows)
})
export default postshttp://localhost:3000/posts пока вернёт пустой массив [] — таблица пуста. Приведение as Post[] нужно, потому что библиотека не знает, какие столбцы вернёт запрос.
Добавление статьи
// src/routes/posts.ts
const result = db
.prepare('INSERT INTO posts (title, body) VALUES (?, ?)')
.run('Первая статья', 'Привет из SQLite')
console.log(result.lastInsertRowid) // номер новой строкиПосле такого вызова список статей вернёт одну запись. Полный набор маршрутов для создания, изменения и удаления — в следующем уроке.
Синхронная работа
better-sqlite3 работает синхронно: методы сразу возвращают результат, без await. Для SQLite это нормально и быстро — файл лежит рядом с программой.
Другие платформы
Пакет better-sqlite3 работает только на Node.js. На Cloudflare Workers вместо него используют D1 — базу SQLite от Cloudflare, она подключается как binding и доступна через c.env. SQL-запросы при этом остаются почти такими же, но методы вызова другие и асинхронные.
Частая ошибка
Данные подставляют в SQL через шаблон строки:
// src/routes/posts.ts
db.prepare(`SELECT * FROM posts WHERE id = ${c.req.param('id')}`).get()Если в адресе окажется кусок SQL, он выполнится. Это называется SQL-инъекцией. Всегда передавайте значения через ? и аргументы метода: db.prepare('SELECT * FROM posts WHERE id = ?').get(id).