Express.jsMiddleExperience
Расскажите о случае, когда вы проектировали или улучшали сервис на Express.js с учётом транзакций, безопасности, фоновых задач или мониторинга.
Для production Express-сервиса ключевые решения: транзакции через переданный клиент БД, JWT-аутентификация с refresh-токенами, фоновые задачи через Bull/BullMQ, мониторинг через Prometheus + pino-structured-logs.
Production-сервис на Express.js: транзакции, безопасность, фоновые задачи, мониторинг
Транзакции с PostgreSQL (node-postgres)
Транзакции требуют единого клиента из пула. Удобный паттерн — хелпер, который передаёт клиент в callback:
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function withTransaction(callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
// Использование в роутере
app.post('/orders', async (req, res, next) => {
try {
const order = await withTransaction(async (client) => {
const { rows } = await client.query(
'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
[req.user.id, req.body.total]
);
await client.query(
'UPDATE inventory SET quantity = quantity - 1 WHERE product_id = $1',
[req.body.productId]
);
return rows[0];
});
res.status(201).json(order);
} catch (err) {
next(err);
}
});
Безопасность: JWT с refresh-токенами
const jwt = require('jsonwebtoken');
const { createClient } = require('redis');
const redis = createClient({ url: process.env.REDIS_URL });
async function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// Проверка blacklist (для отозванных токенов)
const revoked = await redis.get(`revoked:${payload.jti}`);
if (revoked) return res.status(401).json({ error: 'Token revoked' });
req.user = payload;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
}
app.post('/auth/logout', authenticate, async (req, res) => {
const ttl = req.user.exp - Math.floor(Date.now() / 1000);
await redis.setEx(`revoked:${req.user.jti}`, ttl, '1');
res.json({ success: true });
});
Фоновые задачи с BullMQ
const { Queue, Worker } = require('bullmq');
const emailQueue = new Queue('emails', { connection: { host: 'redis' } });
// Постановка в очередь из роутера
app.post('/users', async (req, res, next) => {
try {
const user = await createUser(req.body);
await emailQueue.add('welcome', { userId: user.id, email: user.email }, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});
res.status(201).json(user);
} catch (err) {
next(err);
}
});
// Воркер (отдельный процесс)
new Worker('emails', async (job) => {
if (job.name === 'welcome') {
await sendWelcomeEmail(job.data.email);
}
}, { connection: { host: 'redis' } });
Мониторинг: Prometheus + структурированные логи
const pino = require('pino');
const pinoHttp = require('pino-http');
const promClient = require('prom-client');
// Структурированные логи
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
app.use(pinoHttp({ logger }));
// Prometheus метрики
promClient.collectDefaultMetrics();
const httpDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_code'],
});
app.use((req, res, next) => {
const end = httpDuration.startTimer();
res.on('finish', () => {
end({ method: req.method, route: req.route?.path || req.path, status_code: res.statusCode });
});
next();
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
Подводные камни
- Отсутствие централизованного error handler: необработанные исключения в async-роутерах без
next(err)зависают без ответа клиенту. - Утечка пула соединений: забытый
client.release()после ошибки исчерпывает пул — всегда используйтеfinally. process.on('unhandledRejection')без завершения процесса маскирует критические ошибки; в production падайте явно и дайте PM2/Kubernetes перезапустить.- JWT-секрет из
process.envбез проверки при старте: сервис запустится с undefined-секретом и будет принимать любые токены. - BullMQ без мониторинга (Bull Board / arena) — накопившиеся failed jobs незаметны до тех пор, пока не накопятся тысячи.
/metricsбез IP-whitelist или basic auth доступен публично и раскрывает внутренние метрики.- Синхронные операции в роутерах (fs.readFileSync, crypto с большими данными) блокируют event loop для всех запросов.
What hurts your answer
- Выдумывать опыт или говорить слишком общими фразами
- Не объяснять свою личную роль в работе с Express.js
- Не показывать результат, метрики или извлечённые уроки
What they're listening for
- Может подготовить честный пример использования Express.js
- Показывает свою роль, решения и результат
- Умеет рефлексировать над trade-offs и уроками