Введение: Почему TypeScript и PostgreSQL — идеальная пара для вашего проекта
Вы когда-нибудь задумывались, почему разработчики всё чаще выбирают связку TypeScript и PostgreSQL? Это не просто модный тренд — это мощный тандем, который меняет правила игры в современной веб-разработке. Представьте себе швейцарский нож и надёжный сейф в одном флаконе: TypeScript привносит в код строгость и предсказуемость, а PostgreSQL — надёжность и гибкость хранения данных.
В этом руководстве мы не просто пробежимся по теории. Мы погрузимся в реальную практику: настроим подключение, напишем запросы, создадим API и разберёмся, как избежать типичных ошибок. Готовы? Поехали!
Настройка окружения: с чего начать работу с TypeScript и PostgreSQL
Прежде чем писать код, давайте подготовим почву. Установка зависимостей — это как фундамент дома: чем крепче, тем дольше простоит проект.
Установка необходимых пакетов
Нам понадобятся два главных инструмента: pg (клиент для PostgreSQL) и typescript с типами. Выполните в терминале:
npm init -y
npm install pg
npm install -D typescript @types/node @types/pg
npx tsc --init
Обратите внимание: мы устанавливаем типы для pg отдельно. Это обязательное условие для работы с TypeScript базой данных — иначе компилятор будет ругаться на отсутствие типов.
Базовая структура проекта
Создайте файл database.ts — это будет наше сердце подключения. Вот минимальный, но рабочий шаблон:
import { Pool } from 'pg';
const pool = new Pool({
user: 'your_username',
host: 'localhost',
database: 'your_database',
password: 'your_password',
port: 5432,
});
export default pool;
Почему Pool, а не обычный клиент? Pool управляет пулом соединений, что критически важно для продакшена. Представьте, что каждый запрос — это посетитель кафе. Без пула вы бы открывали новое кафе для каждого клиента. С пулом — у вас уже есть несколько столиков, и вы просто усаживаете гостей.
Основные операции: CRUD на TypeScript с PostgreSQL
Теперь самое интересное — написание запросов. Мы создадим простую таблицу пользователей и реализуем все четыре операции: Create, Read, Update, Delete.
Создание таблицы
Перед работой с данными нужно создать структуру. Вот как это сделать через TypeScript:
import pool from './database';
async function createTable() {
const query = `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER CHECK (age > 0),
created_at TIMESTAMP DEFAULT NOW()
);
`;
try {
await pool.query(query);
console.log('Таблица users создана');
} catch (error) {
console.error('Ошибка:', error);
}
}
createTable();
Обратите внимание на SERIAL — это автоинкремент, который PostgreSQL генерирует сам. А CHECK гарантирует, что возраст будет положительным. Это пример того, как PostgreSQL TypeScript вместе обеспечивают целостность данных на двух уровнях.
Добавление данных (Create)
Вставка с параметрами — безопасный способ избежать SQL-инъекций:
interface User {
name: string;
email: string;
age: number;
}
async function addUser(user: User) {
const query = `
INSERT INTO users (name, email, age)
VALUES ($1, $2, $3)
RETURNING *;
`;
const values = [user.name, user.email, user.age];
try {
const result = await pool.query(query, values);
console.log('Добавлен пользователь:', result.rows[0]);
return result.rows[0];
} catch (error) {
console.error('Ошибка при добавлении:', error);
throw error;
}
}
// Использование
addUser({ name: 'Алексей', email: 'alex@example.com', age: 28 });
Обратите внимание на RETURNING * — эта магия PostgreSQL возвращает только что созданную запись. Очень удобно для получения сгенерированного ID.
Чтение данных (Read)
Запросы с условиями и сортировкой:
async function getUsers(ageLimit?: number) {
let query = 'SELECT * FROM users';
const values: number[] = [];
if (ageLimit) {
query += ' WHERE age > $1';
values.push(ageLimit);
}
query += ' ORDER BY created_at DESC';
try {
const result = await pool.query(query, values);
return result.rows;
} catch (error) {
console.error('Ошибка чтения:', error);
throw error;
}
}
// Получить всех пользователей старше 25 лет
const adultUsers = await getUsers(25);
Заметили, как TypeScript помогает? Интерфейс User гарантирует, что мы не передадим лишние поля, а тип number[] для значений исключает случайную строку в параметре.
Обновление и удаление (Update/Delete)
Обновление с проверкой существования записи:
async function updateUser(id: number, updates: Partial) {
const fields = Object.keys(updates);
if (fields.length === 0) return null;
const setClauses = fields.map((field, index) => `${field} = $${index + 2}`);
const values = [id, ...fields.map(field => updates[field as keyof User])];
const query = `
UPDATE users
SET ${setClauses.join(', ')}
WHERE id = $1
RETURNING *;
`;
try {
const result = await pool.query(query, values);
if (result.rows.length === 0) {
console.log('Пользователь не найден');
return null;
}
return result.rows[0];
} catch (error) {
console.error('Ошибка обновления:', error);
throw error;
}
}
// Частичное обновление
await updateUser(1, { age: 29 });
Удаление по аналогии:
async function deleteUser(id: number) {
const query = 'DELETE FROM users WHERE id = $1 RETURNING *';
try {
const result = await pool.query(query, [id]);
return result.rows[0] || null;
} catch (error) {
console.error('Ошибка удаления:', error);
throw error;
}
}
Продвинутые техники: транзакции и ORM
Когда ваш проект вырастает, сырые запросы становятся сложными в поддержке. Здесь на помощь приходят транзакции и ORM.
Транзакции в TypeScript
Представьте, что вы переводите деньги между счетами. Если один запрос выполнится, а второй нет — баланс нарушится. Транзакции спасают:
import pool from './database';
async function transferMoney(fromId: number, toId: number, amount: number) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const deductQuery = 'UPDATE accounts SET balance = balance - $1 WHERE id = $2';
await