Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

База данных

Статьи из массива в app/data.server.ts пропадают при каждом перезапуске сервера. Их нужно хранить в базе данных. React Router не навязывает конкретную базу: загрузчики и действия — обычный серверный код, из которого можно обращаться к чему угодно. В курсе используется SQLite — база в одном файле, которую не нужно отдельно устанавливать, и пакет better-sqlite3.

Установка

npm install better-sqlite3
npm install -D @types/better-sqlite3

Первая команда ставит драйвер базы, вторая — его типы для TypeScript.

Подключение и таблицы

Создайте файл app/db.server.ts:

// app/db.server.ts
import Database from "better-sqlite3";
 
export const db = new Database("blog.db");
 
db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    body TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    post_id INTEGER NOT NULL REFERENCES posts(id),
    author TEXT NOT NULL,
    text TEXT NOT NULL
  );
`);

При первом запросе к серверу в корне проекта появится файл blog.db с двумя таблицами. CREATE TABLE IF NOT EXISTS не трогает таблицы, которые уже есть. В большом проекте вместо этого используют миграции, но для начала так проще.

Функции данных

Перепишите app/data.server.ts, оставив имена функций прежними:

// app/data.server.ts
import { db } from "./db.server";
 
export type Post = { id: number; title: string; body: string };
export type Comment = { id: number; postId: number; author: string; text: string };
 
export async function getPosts() {
  return db.prepare("SELECT id, title, body FROM posts ORDER BY id DESC").all() as Post[];
}
 
export async function getPost(id: number) {
  const row = db.prepare("SELECT id, title, body FROM posts WHERE id = ?").get(id);
  return (row as Post | undefined) ?? null;
}
 
export async function createPost(title: string, body: string) {
  const result = db.prepare("INSERT INTO posts (title, body) VALUES (?, ?)").run(title, body);
  return { id: Number(result.lastInsertRowid), title, body };
}
 
export async function getComments(postId: number) {
  return db
    .prepare("SELECT id, post_id AS postId, author, text FROM comments WHERE post_id = ?")
    .all(postId) as Comment[];
}
 
export async function addComment(postId: number, author: string, text: string) {
  db.prepare("INSERT INTO comments (post_id, author, text) VALUES (?, ?, ?)").run(postId, author, text);
}

Загрузчики и действия ничего не заметили: они вызывают те же функции. Добавьте статью через форму /posts/new, перезапустите npm run dev — статья на месте.

Методы better-sqlite3

МетодВозвращает
prepare(sql).all(...)Массив строк
prepare(sql).get(...)Одну строку или undefined
prepare(sql).run(...)Сведения о записи: changes, lastInsertRowid

Пакет работает синхронно, поэтому без await; функции модуля остаются async, чтобы базу можно было сменить. Для PostgreSQL или MySQL часто берут ORM — Prisma или Drizzle; подход тот же: подключение в файле .server.ts, вызовы из loader и action.

Частая ошибка

Подставлять значения прямо в текст запроса:

// app/data.server.ts
// Неправильно:
db.prepare(`SELECT * FROM posts WHERE id = ${id}`).get();
// Правильно:
db.prepare("SELECT * FROM posts WHERE id = ?").get(id);

Значение пришло из адреса, и в него можно вписать кусок SQL — это SQL-инъекция. Передавайте значения только через знаки ? и аргументы get, all, run.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.