Как могут утечь все соединения из пула коннекшенов к базе данных и как это предотвратить?
Утечка соединений — это критическая проблема в Production, которая приводит к падению приложения. Когда соединения заканчиваются, новые запросы не могут подключиться к БД.
Как утекают соединения?
Причина #1: Не закрыли соединение после использования
// ✗ ПЛОХО: Соединение никогда не вернулось в пул
import { pool } from './db';
async function getUser(id: string) {
const connection = await pool.getConnection();
const user = await connection.query('SELECT * FROM users WHERE id = $1', [id]);
// ✗ Забыли connection.release()!
return user;
}
// После 10 запросов (размер пула = 10): все соединения исчерпаны
// Следующий запрос: Error: connect ECONNREFUSED (не может получить соединение)
Причина #2: Ошибка в коде блокирует release()
Что такое back pressure для стримов и какая проблема была бы без него?
Back pressure (обратное давление) — это механизм в Node.js Streams для управления скоростью передачи данных, когда потребитель данных (Writable Stream) обрабатывает медленнее, чем источник (Readable Stream) отправляет. Это предотвращает переполнение памяти.
Проблема без Back Pressure
// ПЛОХО — без управления скоростью
const fs = require('fs');
const readStream = fs.createReadStream('./huge-file.txt');
const writeStream = fs.createWriteStream('./output.txt');
readStream.on('data', (chunk) => {
writeStream.write(chunk);
});
// Что происходит:
// 1. Читаем chunk 1 (100KB) → буфер: 100KB
// 2. Читаем chunk 2 (100KB) → буфер: 200KB
// 3. Читаем chunk 3 (100KB) → буфер: 300KB
// ...
// 100. Читаем chunk 100 (100KB) → буфер: 10GB (!!! — ПРОБЛЕМА)
Как работает Back Pressure
Что такое массив?
Массив — это упорядоченная коллекция элементов, хранящихся в одной переменной. Это один из фундаментальных типов данных в программировании, позволяющий работать с несколькими значениями одновременно. Каждый элемент массива имеет индекс (порядковый номер), начинающийся с 0.
Основные характеристики массива
Примеры создания массивов в JavaScript
// Литеральная нотация
const numbers = [1, 2, 3, 4, 5];
const mixed = [1, "строка", true, null];
const empty = [];
// Конструктор Array
const arr = new Array(3); // [empty, empty, empty]
const arr2 = new Array(1, 2, 3); // [1, 2, 3]
Основные операции с массивами
Мои профессиональные цели как Backend разработчика
Как эксперт с 10+ лет опыта, мои цели сосредоточены на архитектуре, лидерстве и влиянии.
Краткосрочные цели (1-2 года)
Стать архитектором масштабных систем
const system = {
apiGateway: 'Load Balancer',
services: [
'user-service (PostgreSQL)',
'order-service (PostgreSQL)',
'notification-service (RabbitMQ)'
],
cache: 'Redis cluster',
analytics: 'ClickHouse'
};
Углубить знания в Distributed Systems
Среднесрочные цели (2-5 лет)
Стать technical leader в команде
Опыт работы с legacy-проектами и code handover
Это важный вопрос для собеседования, так как большинство backend-разработчиков в своей карьере работают с уже существующими проектами, которые кто-то создал до них.
Типичная ситуация: получить проект "не с нуля"
Сценарий:
Что нужно сделать при handover проекта
1. Изучение кодовой базы
# Начальный анализ проекта
git log --oneline -10 # Последние коммиты
git log --stat --decorate main # История изменений
git show <commit-hash> # Что изменилось в коммите
git blame app.js # Кто что писал
# Размер проекта
wc -l **/*.js # Количество строк
find . -type f -name "*.js" | wc -l # Количество файлов
2. Понимание архитектуры
Обработка uncaught exceptions и unhandled rejections
В Node.js необработанные исключения и отклоненные промисы могут привести к аварийному завершению процесса и потере данных. Правильная обработка этих ситуаций критична для production-приложений.
Uncaught Exceptions
Uncaught exception возникает, когда синхронное исключение выбрасывается вне блока try/catch:
function processData(data: unknown) {
const parsed = JSON.parse(data as string);
return parsed.value.nested.prop;
}
// setTimeout callback - нет обертки try/catch
setTimeout(() => {
processData("invalid json"); // uncaught exception!
}, 1000);
Обработка через process events
process.on("uncaughtException", (error: Error, origin: string) => {
console.error("Uncaught Exception:", {
message: error.message,
stack: error.stack,
origin
});
// Отправить в систему мониторинга
logger.fatal("Uncaught exception", { error });
Аутентификация с JWT в Node.js
JWT (JSON Web Token) — это стандартный способ передачи данных между клиентом и сервером в защищённом виде.
Структура JWT
JWT состоит из трёх частей: header.payload.signature
Установка зависимостей
npm install jsonwebtoken express bcryptjs dotenv
Генерация и проверка JWT
const jwt = require("jsonwebtoken");
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
// Генерация access токена
function generateAccessToken(userId) {
return jwt.sign(
{ userId, role: "user" },
ACCESS_TOKEN_SECRET,
{ expiresIn: "15m" }
);
}
// Генерация refresh токена
function generateRefreshToken(userId) {
return jwt.sign(
{ userId },
REFRESH_TOKEN_SECRET,
{ expiresIn: "7d" }
);
}
Async/Await в JavaScript — синтаксический сахар для Promise
Async/Await — это современный способ написания асинхронного кода, который выглядит синхронным, но работает асинхронно.
История развития
// ❌ Callback hell
function fetchUserData(id, callback) {
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => {
fetch(`/api/posts/${user.id}`)
.then(res => res.json())
.then(posts => callback(posts))
})
}
// ✅ Async/Await (современно)
async function fetchUserData(id) {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
const posts = await fetch(`/api/posts/${user.id}`).then(r => r.json());
return posts;
}
Синтаксис
async function getData() {
const data = await fetch('/api/data');
return data;
}
const promise = getData(); // Всегда возвращает Promise
Обработка ошибок
Как реализованы тесты на проекте?
Тестирование — не хобби, а обязательная часть разработки. На моем проекте используется многоуровневая стратегия тестирования с высокой coverage и fast feedback loop. Покажу реальный пример архитектуры.
Pyramid Testing: правильная структура
Использую тестовую пирамиду:
┌─────────────────┐
│ E2E Тесты │ 5% (Google Chrome)
├─────────────────┤
│ Integration │ 20% (API, БД, очереди)
├─────────────────┤
│ Unit │ 75% (функции, классы)
└─────────────────┘
Unit тесты (75%)
Integration тесты (20%)
E2E тесты (5%)
Асинхронность в Node.js: глубокий разбор
Node.js построен на асинхронности. Это его ДНК. Разберу пошагово, как всё устроено:
Event Loop — сердце Node.js
Event Loop — это бесконечный цикл, который обрабатывает события:
// Упрощённо:
while (eventLoop.waitForTask()) {
const task = eventLoop.nextTask();
task.execute();
}
Event Loop имеет несколько фаз:
Callback Queue и Microtask Queue
Есть не одна очередь, а несколько:
// Microtask Queue (выше по приоритету)
Promise.then()
process.nextTick()
queueMicrotask()
// Callback Queue (ниже по приоритету)
setTimeout()
setInterval()
setImmediate()
Как передаются параметры функций в JavaScript
Передача параметров в JavaScript основана на принципе pass-by-value для примитивов и pass-by-reference для объектов. Понимание этого различия критично для избежания неожиданных ошибок.
1. Примитивные типы — передача по значению
Для примитивов (number, string, boolean, null, undefined, symbol, bigint) создаётся копия значения. Изменение параметра внутри функции не влияет на исходную переменную.
function modify(value) {
value = value * 2;
console.log('Inside function:', value); // 20
}
let num = 10;
modify(num);
console.log('Outside function:', num); // 10 (не изменилось)
Почему? JavaScript создаёт копию значения в стеке памяти:
До вызова: После вызова:
num = 10 num = 10 (в основном scope)
value = 10 (копия в scope функции)
value = 20 (изменение копии)
То же самое с строками и другими примитивами:
Express как фреймворк для Node.js
Да, Express — это легковесный но мощный веб-фреймворк для Node.js. Это один из самых популярных фреймворков в экосистеме, и я использовал его в десятках проектов.
Что такое Express
Express — это фреймворк для:
const express = require('express');
const app = express();
app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
app.listen(3000, () => console.log('Server running'));
Концепция Middleware
Это сердце Express. Middleware — это функции, которые имеют доступ к request и response объектам:
app.use((req, res, next) => {
console.log('Request received');
next(); // передать управление следующему middleware
});
app.use((req, res, next) => {
req.startTime = Date.now();
next();
});
app.get('/api/users', (req, res) => {
res.json({ data: [] });
});
INNER, LEFT и RIGHT JOIN
Это три способа объединения таблиц в SQL с разными стратегиями выбора строк.
INNER JOIN
Возвращает только строки, где есть совпадение в обеих таблицах.
SELECT * FROM users
INNER JOIN orders ON users.id = orders.user_id;
Пример:
users: 1-Alice, 2-Bob, 3-Charlie
orders: user_id=1, user_id=2
Результат: Alice(1), Bob(2)
Charlie исключена (нет заказов)
Node.js:
const result = await userRepository
.createQueryBuilder('user')
.innerJoin('user.orders', 'order')
.getMany();
LEFT JOIN (LEFT OUTER JOIN)
Возвращает все строки из левой таблицы + совпадающие из правой. Несовпадающие из правой будут NULL.
SELECT * FROM users
LEFT JOIN orders ON users.id = orders.user_id;
Пример:
users: 1-Alice, 2-Bob, 3-Charlie
orders: user_id=1, user_id=2
Результат:
Alice(1) with order
Bob(2) with order
Charlie(3) with NULL (нет заказов)
Как реализовать глобальный error-handling middleware в Express?
Глобальный error-handling middleware — это функция, которая ловит все ошибки в Express приложении и обрабатывает их в одном месте.
Базовая структура
import express, { ErrorRequestHandler } from 'express';
const app = express();
// 1. Все остальные middleware и роуты
// 2. Error handling middleware (ДОЛЖНО быть в конце!)
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ error: err.message });
};
app.use(errorHandler);
Полный пример с типизацией
import express, { ErrorRequestHandler, Request, Response, NextFunction } from 'express';
interface ApiError extends Error {
status?: number;
}
const app = express();
Опыт работы с различными базами данных
Я работал с различными типами баз данных, и каждая имеет свои сильные стороны. Расскажу о моём опыте и подходе к выбору БД для разных задач.
PostgreSQL — основная БД для большинства проектов
Это мой выбор по умолчанию для 80% приложений. Почему:
// Пример работы с PostgreSQL в Node.js
const { Pool } = require('pg');
const pool = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME
});
// Параметризованные запросы (защита от SQL injection)
const getUserById = async (userId) => {
const result = await pool.query(
'SELECT id, email, role FROM users WHERE id = $1',
[userId]
);
return result.rows[0];
};
Способы объявления переменных в JavaScript
В JavaScript существует три основных способа объявления переменных, каждый из которых имеет свои особенности, область видимости и поведение.
1. var — устаревший способ
var был единственным способом объявления переменных до ES2015. Хотя он всё ещё работает, его использование не рекомендуется в современном коде.
var name = 'John';
var age = 30;
Характеристики:
undefinedundefinedfunction test() {
console.log(x); // undefined (hoisting)
var x = 10;
}
2. let — рекомендуемый способ для переменных
let введён в ES2015 и решает проблемы var. Это оптимальный выбор для большинства случаев.
Зачем нужен async/await?
async/await — это синтаксический сахар для работы с промисами, который делает асинхронный код похожим на синхронный. Это одна из самых важных фич современного JavaScript/Node.js.
Проблемы, которые решает async/await
Callback Hell (адский ад колбэков):
// Без async/await - адский ад вложенности
fs.readFile("file1.txt", (err, data1) => {
if (err) throw err;
fs.readFile("file2.txt", (err, data2) => {
if (err) throw err;
fs.readFile("file3.txt", (err, data3) => {
if (err) throw err;
console.log(data1, data2, data3);
});
});
});
С async/await — чистый и понятный код:
async function readFiles() {
try {
const data1 = await fs.promises.readFile("file1.txt");
const data2 = await fs.promises.readFile("file2.txt");
const data3 = await fs.promises.readFile("file3.txt");
console.log(data1, data2, data3);
} catch (err) {
throw err;
}
}
Основные преимущества
Для чего используется Foreign Key в БД
Foreign Key (внешний ключ) — это механизм реляционной БД, который обеспечивает целостность данных через связи между таблицами. Это один из столпов relational databases.
Основная функция
Foreign Key создаёт связь между двумя таблицами:
-- Главная таблица
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
-- Зависимая таблица с FOREIGN KEY
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
content TEXT,
user_id INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
Основные функции Foreign Key
1. Обеспечение referential integrity (целостности ссылок)
БД гарантирует, что в таблице posts не может быть user_id, который не существует в users.
Что такое CRUD?
CRUD — это аббревиатура четырёх базовых операций для работы с данными:
C — Create (Создание)
R — Read (Чтение)
U — Update (Обновление)
D — Delete (Удаление)
Пример API эндпоинтов:
// CREATE — добавить пользователя
POST /users
Body: { name: "John", email: "john@example.com" }
// READ — получить пользователя
GET /users/1
// UPDATE — изменить пользователя
PUT /users/1
Body: { name: "Jane" }
// DELETE — удалить пользователя
DELETE /users/1
Пример базы данных:
-- CREATE
INSERT INTO users (name, email) VALUES ('John', 'john@example.com');
-- READ
SELECT * FROM users WHERE id = 1;
-- UPDATE
UPDATE users SET name = 'Jane' WHERE id = 1;
-- DELETE
DELETE FROM users WHERE id = 1;
Самые часто используемые функции в SQL
SQL включает множество встроенных функций для обработки данных. Рассмотрим самые важные и часто используемые из них в повседневной разработке.
Функции для работы со строками
CONCAT или оператор || объединяет строки:
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
-- или
SELECT first_name || ' ' || last_name AS full_name FROM users;
SUBSTRING извлекает часть строки:
SELECT SUBSTRING(email FROM 1 FOR 5) FROM users;
UPPER/LOWER преобразуют регистр:
SELECT UPPER(name), LOWER(email) FROM users;
TRIM/LTRIM/RTRIM удаляют пробелы:
SELECT TRIM(name) FROM users;
Функции для работы с числами
ABS возвращает абсолютное значение:
SELECT ABS(-100);
ROUND округляет число:
SELECT ROUND(price, 2) FROM products;
CEIL/FLOOR округляют в большую/меньшую сторону:
SELECT CEIL(3.2), FLOOR(3.9);
Агрегирующие функции
Какие знаешь инструменты для работы с БД в Node.js?
За 10+ лет разработки я работал с различными инструментами для работы с базами данных в Node.js. Расскажу о самых популярных и проверенных временем решениях, их преимуществах и недостатках.
Основные категории инструментов
1. ORM (Object-Relational Mapping)
ORM — это слой абстракции, который позволяет работать с БД через объекты вместо SQL.
Одна из самых популярных ORM для Node.js.
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('database', 'user', 'password', {
host: 'localhost',
dialect: 'postgres'
});
// Определение модели
const User = sequelize.define('User', {
email: DataTypes.STRING,
name: DataTypes.STRING,
age: DataTypes.INTEGER
});
// Создание
await User.create({ email: 'john@example.com', name: 'John', age: 30 });
// Поиск
const user = await User.findByPk(1);
// Обновление
await user.update({ age: 31 });
Как проверял токен в приложении?
Проверка токенов — одна из ключевых задач аутентификации и авторизации в backend приложениях. За 10+ лет работы с Node.js я использовал различные подходы для валидации токенов.
Типы токенов
Основные типы токенов, которые я проверял:
JWT проверка
const jwt = require('jsonwebtoken');
Для чего нужен Redis?
Redis — это высокопроизводительное хранилище данных в памяти (in-memory data store), которое используется как кэш, очередь сообщений, сессионное хранилище и для синхронизации между сервисами.
Основные характеристики Redis
Redis работает с данными в оперативной памяти, что делает его невероятно быстрым:
Основные сценарии использования
1. Кэширование данных
Это самый частый сценарий. Redis кэширует результаты дорогостоящих операций, чтобы не обращаться к БД.
Пример:
const redis = require('redis');
const client = redis.createClient();
Promise в JavaScript/Node.js
Promise — это объект в JavaScript, который представляет результат асинхронной операции (может быть успешно выполнена или отклонена). Это фундамент асинхронного программирования в современном Node.js.
Определение
Promise — это объект, который содержит:
Жизненный цикл Promise
// Состояние: PENDING (ожидание)
const promise = new Promise((resolve, reject) => {
// resolve() или reject() изменит состояние
});
// После вызова resolve() → FULFILLED (выполнено)
// После вызова reject() → REJECTED (отклонено)
// Состояние не может измениться обратно!
Базовый синтаксис
Мой профессиональный опыт
Я Backend разработчик с 10+ годами опыта в проектировании и разработке масштабируемых приложений на Node.js. За этот период я работал над десятками проектов различных масштабов — от небольших стартапов до крупных корпоративных систем.
Ранний опыт (2014-2016)
Мой путь в разработке начался с фронтенда, но я быстро перешел на бэкенд, когда понял, что именно там происходит вся интересная логика. Начал с Express.js, строил RESTful API для веб-приложений. Тогда основной фокус был на базовых навыках: работа с HTTP, маршрутизация, управление зависимостями.
Продвинутый опыт (2016-2019)
Что такое SQL injection и как защитить Node.js приложение от него?
SQL Injection — это критическая уязвимость безопасности, при которой злоумышленник внедряет вредоносный SQL код в пользовательские входные данные.
Как происходит SQL Injection
const userId = req.query.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Если userId = "1 OR 1=1", то SELECT * FROM users WHERE id = 1 OR 1=1 вернёт ВСЕ пользователей
const username = req.body.username;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '...'`;
// Если username = "admin' --", то -- комментарий игнорирует остаток запроса
1. Защита через Parameterized Queries
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection({ ... });
const userId = req.query.id;
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
2. ORM (Object-Relational Mapping)
Promise.all: параллельное выполнение Promise'ов
Что такое Promise.all
Promise.all это метод, который принимает массив Promise'ов и возвращает новый Promise. Результат:
Синтаксис и примеры
// Базовый пример
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(results => {
console.log(results); // [1, 2, 3]
});
// С async/await
const results = await Promise.all([p1, p2, p3]);
console.log(results); // [1, 2, 3]
Практический пример: Загрузка данных из разных API
Опыт работы с различными БД
За 10+ лет разработки я работал с большим спектром баз данных, каждая из которых имеет свои преимущества и use case'ы. Расскажу о наиболее значимом опыте.
PostgreSQL (основной выбор)
Это моя primary БД для 80% проектов. PostgreSQL идеален для:
interface UserMetadata {
preferences: {
language: string;
theme: 'light' | 'dark';
};
subscription_tier: 'free' | 'pro' | 'enterprise';
}
const users = await db.query(`
SELECT * FROM users
WHERE data->>'language' = $1
AND (data->>'subscription_tier')::text = $2
`, ['en', 'pro']);
MongoDB (документная БД)
Использовал в проектах где нужна гибкость схемы:
Работа с московским временем в Node.js Backend
В backend разработке работа с временем и таймзонами — критический вопрос, особенно для русских сервисов. Вот полный разбор:
1. Основные концепции
Текущее время и московский timezone:
import { DateTime } from 'luxon';
// Московский timezone (MSK, UTC+3)
const moscow = 'Europe/Moscow';
// Получить текущее время
const nowUTC = new Date(); // UTC время
const nowMoscow = DateTime.now().setZone('Europe/Moscow');
// UTC: 2024-01-15 10:00:00
// Moscow: 2024-01-15 13:00:00 (UTC+3)
// Разница: 3 часа вперёд
Стандартная разница:
Moscow (MSK): UTC+3 (год-круглый, без летнего времени)
Примеры:
- Лондон (GMT): MSK на 3 часа вперёд
- Нью-Йорк (EST): MSK на 8 часов вперёд
- Нью-Йорк (EDT): MSK на 7 часов вперёд
- Пекин (CST): MSK на 5 часов позади
2. Best Practices для backend
Правило 1: Храни всё в UTC
Path Traversal уязвимость
Path Traversal (Directory Traversal) - это тип уязвимости, при которой злоумышленник может получить доступ к файлам и директориям, расположенным за пределами предполагаемой корневой директории веб-приложения. Атака использует недостаточную валидацию пользовательского ввода при формировании путей к файлам.
Как работает атака
Злоумышленник манипулирует параметрами запроса, используя специальные последовательности вроде ../ для выхода за пределы разрешённой директории:
// Уязвимый код
app.get("/files", (req, res) => {
const fileName = req.query.name;
const filePath = path.join(__dirname, "uploads", fileName);
res.sendFile(filePath); // опасно!
});
// Запрос: GET /files?name=../../../etc/passwd
// Результат: доступ к системному файлу
Основные векторы атаки
UDP — User Datagram Protocol
UDP (User Datagram Protocol) — это протокол транспортного слоя (Layer 4 в модели OSI), который предоставляет быстрое, но ненадёжное доставку данных между приложениями в сети.
Основные характеристики UDP
1. Без установления соединения UDP отправляет данные сразу, без предварительного рукопожатия, как это делает TCP.
2. Ненадёжная доставка UDP не гарантирует:
3. Низкие задержки (Low Latency) Без overhead соединения UDP работает быстрее, чем TCP.
4. Меньший размер заголовка UDP заголовок 8 байт, TCP заголовок 20+ байт.
UDP vs TCP
JOIN операции в SQL: Практический опыт
Да, я постоянно работаю с JOIN в реальных проектах. Более того, правильное использование JOIN — это один из ключевых навыков backend разработчика. За 10+ лет я стал экспертом в оптимизации запросов с JOIN для production систем. Плохо спроектированные JOIN могут привести к 100x замедлению, а правильно спроектированные — ускорить систему в 10x раз.
Типы JOIN операций
INNER JOIN — только matching записи:
SELECT u.id, u.username, p.title
FROM users u
INNER JOIN posts p ON u.id = p.user_id
WHERE u.created_at > '2024-01-01';
Это my go-to для большинства случаев. INNER JOIN гарантирует что мы получим только те юзеры которые имеют посты.
LEFT JOIN — все записи из левой таблицы + matching из правой:
SELECT u.id, u.username, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id;
Осторожно с LEFT JOIN + COUNT — нужно считать distinct если есть multiple matches:
Сроки адаптации при выходе на новую роль
Этот вопрос очень важен для работодателя, так как напрямую влияет на планирование спринтов и распределение задач. Дам честный и реалистичный ответ на основе моего опыта.
Моя оценка: 3-4 недели для полной продуктивности
Фаза 1: Onboarding (неделя 1)
Это самый интенсивный период адаптации:
День 1-2: Setup окружения
День 3-4: Изучение кодовой базы
День 5: Первая простая задача
Git-практики и workflows в моей практике
Гит — это инструмент, с которым я работаю ежедневно последние 10 лет, и пережил эволюцию от простого версионирования к сложным многофилиальным workflows с десятками разработчиков.
Основные Git-операции, которые я применяю
Ветвление и feature branching:
# Создаю feature ветку с понятным именем
git checkout -b feature/payment-gateway-integration
# Работаю изолировано
git add src/services/payment.js
git commit -m "feat: add Stripe integration"
# Обновляю ветку перед merge request
git fetch origin
git rebase origin/main
# Решаю конфликты при необходимости
git status
# ... исправляю конфликты вручную ...
git add .
git rebase --continue
###策略и workflows
Git Flow для крупных проектов:
Promise.all и обработка ошибок
Нет, Promise.all остановится и вернет rejected Promise при первой ошибке, даже если остальные Promise"ы еще выполняются. Это важный аспект асинхронного программирования в JavaScript/Node.js, который часто упускают на собеседованиях.
Как это работает
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Ошибка в promise2")), 100);
});
const promise3 = new Promise((resolve) => {
setTimeout(() => resolve(10), 200);
});
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values)) // НЕ выполнится
.catch(error => console.error(error)); // Выполнится: Error: Ошибка в promise2
Результат:
Error: Ошибка в promise2
Обратите внимание: promise3 мог успешно разрешиться за 200ms, но Promise.all уже упал после ошибки в promise2 на 100ms.
Демонстрация с API запросами
Почему Node.js считается однопоточным языком программирования
Это частый вопрос на интервью, и ответ неочевидный, потому что Node.js НА САМОМ ДЕЛЕ не полностью однопоточный. Давайте разберёмся.
Что имеют в виду под "однопоточный"
JavaScript код выполняется в одном потоке (Main Thread):
// Этот код выполняется в ОДНОМ потоке
console.log('Start');
setTimeout(() => {
console.log('Middle'); // Выполнится в том же потоке
}, 1000);
console.log('End');
// Вывод:
// Start
// End
// Middle (через 1 секунду)
Нет параллельного выполнения:
// ❌ Это НЕ возможно:
thread1: sum = 0;
thread2: sum = 1; // Параллельно в другом потоке
// Результат не определён
// ✅ В Node.js всегда упорядоченно:
sum = 0;
sum = 1;
// Всегда в одном потоке, всегда предсказуемо
Архитектура Node.js
Что такое Migrations в БД?
Migrations (миграции) — это версионный контроль для схемы базы данных. Это как git для структуры таблиц, но для БД. Я использую их ежедневно в production коде.
Проблема без миграций
Деньги 1: Создал таблицу users вручную
CREATE TABLE users (id INT, name VARCHAR(100));
День 2: Добавил email колонку вручную
ALTER TABLE users ADD COLUMN email VARCHAR(100);
День 3: Хочу дать это другому разработчику
Как ему передать эту информацию?
- Отправить SQL скрипт? (запутано)
- Сказать что-то вроде "выполни эти команды в БД"? (неправильно)
- Дать дамп БД? (ОГРОМНЫЙ файл)
День 10: На production неправильная схема
Эта колонка не индексирована, поэтому медленно
Кто это создал? Когда? Почему?
Что такое Migration
Migration — это файл с SQL/код, который описывает изменение схемы БД.
Какой фреймворк используешь чаще всего
За 10 лет я использовал много фреймворков. Каждый имеет свой контекст.
Мой выбор: Nest.js
Nest.js — основной фреймворк для production приложений.
Почему Nest.js
1. Архитектура из коробки Nest.js навязывает чистую архитектуру:
2. TypeScript first
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
constructor(private usersService: UsersService) {}
@Get(':id')
@UseInterceptors(TransformInterceptor)
async getUser(@Param('id') id: string): Promise<UserDto> {
return this.usersService.getUser(id);
}
}
3. Встроенная поддержка
4. Огромное сообщество Множество документации и решений.
Какие знаешь фреймворки Node.js?
Выбор правильного фреймворка — важный шаг при разработке backend приложения. За 10+ лет работы я использовал практически все популярные фреймворки и понимаю их особенности и применение.
1. Express.js
Самый популярный и лёгкий фреймворк:
import express, { Request, Response } from 'express';
const app = express();
// Middleware
app.use(express.json());
// Маршруты
app.get('/users/:id', async (req: Request, res: Response) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/users', async (req: Request, res: Response) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
app.listen(3000, () => console.log('Server started'));
Преимущества:
Виды тестов в backend разработке
Это не просто знание терминов, но понимание, когда использовать каждый. Расскажу о пирамиде тестов:
Пирамида тестов (по объему)
E2E Tests (10%)
/ \
/ \
Integration Tests (30%)
/ \
/ \
Unit Tests (60%)
1. Unit тесты (60% от всех)
Определение: Тестируешь отдельную функцию/метод изолированно
// Функция для тестирования
function calculateDiscount(price, discountPercent) {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid discount');
}
return price * (1 - discountPercent / 100);
}
Принципы REST приложений
Что такое REST
REST (Representational State Transfer) — это архитектурный стиль для проектирования веб-сервисов, разработанный Roy Fielding в 2000 году. REST определяет набор ограничений (constraints), которые делают веб-сервисы простыми, масштабируемыми и надежными.
Основные принципы REST
1. Архитектура Client-Server
Клиент и сервер работают независимо друг от друга:
Клиент Сервер
↓ ↓
+------- HTTP запрос -----→
←------ HTTP ответ -------+
↓ ↓
УИ, логика клиента Бизнес-логика, БД
Преимущества:
// ✅ REST подход: сервер ничего не знает про клиента
app.get('/api/users/:id', (req, res) => {
const user = getUserFromDB(req.params.id);
res.json(user); // Клиент сам решит, что с этим делать
});
Идемпотентность HTTP-методов: архитектурная необходимость
Идемпотентность — свойство операции, при котором повторное выполнение дает тот же результат, что и первое выполнение. Это критическое требование для надежных распределенных систем.
Почему идемпотентность необходима
Проблема в сетевых системах:
Без идемпотентности рискуем:
Клиент: "Переведи 100 рублей"
Сервер: Выполнил, но ответ потерялся
Клиент: Timeout, повтор запроса
Сервер: Выполнил ещё раз
Результат: Перевод выполнен ДВАЖДЫ!
Какие методы должны быть идемпотентны
GET — получение данных:
GET /api/users/123
GET /api/users/123 // повтор — нет побочных эффектов
Как указать тип параметра в методе
В Node.js есть несколько способов указывать типы параметров в методах. Выбор зависит от стека и требований проекта.
1. TypeScript (рекомендуемый подход)
Типичная современная практика в production-среде.
// Примитивные типы
function greet(name: string, age: number, active: boolean): void {
console.log(`${name} is ${age} years old`);
}
greet('Alice', 30, true); // ✅ OK
greet('Bob', '25', true); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
// Интерфейсы
interface User {
id: number;
name: string;
email?: string; // Опциональное поле
}
function createUser(user: User): User {
return { ...user, id: Math.random() };
}
// Типы
type Status = 'active' | 'inactive' | 'pending';
function updateUserStatus(userId: number, status: Status): void {
console.log(`User ${userId} status: ${status}`);
}
Адаптация на новом месте
Мой подход к адаптации
Адаптация — это не срок, это процесс. У меня есть проверенная система, которая помогает как мне, так и команде:
Первая неделя: погружение
День 1-2: Структура и люди
День 3-4: Окружение
День 5: Первая задача
Первый месяц: вклад
Как я поддерживаю свой уровень как разработчик
С более чем 10-летним опытом я понимаю, что стагнация — враг профессионала. Вот мой подход к постоянному развитию:
Практический опыт
Основа всего — реальные проекты. Я активно работаю с современным стеком: Node.js, TypeScript, микросервисами, containerization. Каждый проект — возможность столкнуться с новыми вызовами и найти лучшие решения.
Изучение новых технологий
Систематический подход:
Изучение теории и архитектуры
Практика без теории ведёт к техдолгу. Я:
Отношение к трудностям в разработке
Отношение к проблемам и трудностям — это то, что отличает профессионалов от любителей. За 10+ лет в разработке я сформировал ясную философию работы с трудностями.
Трудности как возможность, а не угроза
Психология: Когда встречаю сложную задачу или баг, я не говорю себе "Это сложно, может быть, я не справлюсь". Вместо этого думаю: "Интересная задача, здесь я что-то нового выучу".
Практический пример: Сталкнулся с race condition в code, когда несколько потоков обновляют одну таблицу одновременно. Вместо паники:
Систематический подход к решению проблем
1. Спокойствие — первый шаг
Когда в production падает критичный сервис, нужно:
Как оптимизировать запросы к БД?
Это один из критически важных навыков backend разработчика. Неоптимизированные запросы — главная причина медленности приложений. Я расскажу про комплексный подход, который использую.
Шаг 1: Профилирование (EXPLAIN ANALYZE)
Перыми я всегда профилирую запрос, чтобы понять, где проблема.
EXPLAIN ANALYZE
SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.id, u.name
ORDER BY order_count DESC;
На что смотреть:
Интерпретация вывода:
Plan:
Seq Scan on users u (cost=0.00..35.50 rows=1000) ← плохо, Sequential Scan
Filter: (created_at > '2023-01-01')
Execution time: 152.45 ms
Event Emitter в Node.js
EventEmitter — это паттерн в Node.js, который позволяет объектам генерировать события и другим объектам слушать эти события. Это основа асинхронного программирования в Node.js.
Базовая идея
Вместо того чтобы функция возвращала результат, она генерирует события. Код, который заинтересован в этих событиях, слушает их.
const EventEmitter = require('events');
const emitter = new EventEmitter();
// Слушаем событие 'message'
emitter.on('message', (data) => {
console.log('Получили сообщение:', data);
});
// Генерируем событие
emitter.emit('message', 'Hello, World!');
// Вывод: "Получили сообщение: Hello, World!"
Основные методы
1. on() — слушать событие
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
// Каждый раз когда event происходит
emitter.on('data', (value) => {
console.log('Данные:', value);
});
Где находится информация в POST-запросе
ПOST-запрос используется для отправки данных на сервер с целью создания нового ресурса или выполнения операции. В отличие от GET-запроса, где данные передаются в URL, информация в POST может находиться в разных местах, и это критически важно понимать для backend разработчика.
Основные местоположения данных в POST
1. Тело запроса (Body) — основное место
Это наиболее распространённое место для передачи данных в POST-запросе:
// HTTP POST запрос
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 35
{"name": "John", "email": "john@example.com"}
В Node.js/Express для доступа к телу используется middleware:
const express = require('express');
const app = express();
// Парсинг JSON из тела
app.use(express.json());
Дополнительные проекты и side projects
Да, я всегда поддерживаю активность вне основной работы. Я считаю, что это критично для профессионального развития.
Мотивация для side projects
Примеры проектов
Проект 1: CLI инструмент для автоматизации деплоя (Node.js + Commander.js)
// cli-deploy-tool/index.ts
import { Command } from 'commander';
import { deployToProduction } from './commands/deploy';
import { checkHealth } from './commands/health';
const program = new Command();
program
.name('deploy-cli')
.description('Automated deployment tool')
.version('1.0.0');