TypeScript и PostgreSQL: полное руководство по созданию надежного бэкенда

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему 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                

Блоги

Рекомендаций по книгам