22. Fastify: быстрая альтернатива

Fastify — высокопроизводительный Node.js фреймворк. До 2x быстрее Express, встроенная валидация, плагины, TypeScript-friendly.
Почему Fastify?
Заголовок раздела «Почему Fastify?»Express Fastify──────────────── ────────────────~15,000 req/s ~30,000 req/sНет валидации JSON Schema встроенНет TypeScript TypeScript из коробкиcallback-based async/await нативноmiddleware цепи плагины (инкапсуляция)Огромная экосист. Растущая экосистемаБыстрый старт
Заголовок раздела «Быстрый старт»npm install fastifyconst fastify = require('fastify');
const app = fastify({ logger: true, // встроенный логгер pino});
// GET роутapp.get('/', async (request, reply) => { return { message: 'Привет от Fastify!' }; // return автоматически сериализует JSON});
// POST с валидацией через JSON Schemaapp.post('/users', { schema: { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string', minLength: 2, maxLength: 50 }, email: { type: 'string', format: 'email' }, age: { type: 'integer', minimum: 0 }, }, }, response: { 201: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, email: { type: 'string' }, }, }, }, },}, async (request, reply) => { const { name, email, age } = request.body; const user = { id: 1, name, email, age }; reply.status(201); return user;});
// Запускapp.listen({ port: 3000, host: '0.0.0.0' }, (err) => { if (err) { app.log.error(err); process.exit(1); }});Роутинг
Заголовок раздела «Роутинг»// Параметры URLapp.get('/users/:id', async (request) => { const { id } = request.params; return { id };});
// Query параметрыapp.get('/search', { schema: { querystring: { type: 'object', properties: { q: { type: 'string' }, page: { type: 'integer', default: 1 }, limit: { type: 'integer', default: 10, maximum: 100 }, }, required: ['q'], }, },}, async (request) => { const { q, page, limit } = request.query; return { query: q, page, limit, results: [] };});
// Wildcardapp.get('/files/*', async (request) => { return { path: request.params['*'] };});Плагины — основа архитектуры
Заголовок раздела «Плагины — основа архитектуры»// plugins/db.js — плагин для БДconst fp = require('fastify-plugin');
async function dbPlugin(fastify, opts) { const db = await connectToDatabase(opts.url);
// Декорируем — добавляем db ко всем запросам fastify.decorate('db', db);
// Закрываем при выключении fastify.addHook('onClose', async () => { await db.disconnect(); });}
module.exports = fp(dbPlugin);// app.js — регистрация плагиновconst app = fastify({ logger: true });
// Плагиныapp.register(require('./plugins/db'), { url: process.env.DATABASE_URL,});app.register(require('@fastify/cors'), { origin: ['http://localhost:3000'], credentials: true,});app.register(require('@fastify/helmet'));
// Роутыapp.register(require('./routes/users'), { prefix: '/api/users' });app.register(require('./routes/auth'), { prefix: '/api/auth' });// routes/users.js — роуты как плагинasync function userRoutes(fastify, opts) { // fastify.db доступен через плагин
fastify.get('/', async (request) => { const users = await fastify.db.user.findMany(); return { data: users }; });
fastify.get('/:id', async (request, reply) => { const user = await fastify.db.user.findUnique({ where: { id: parseInt(request.params.id) }, }); if (!user) { reply.status(404); return { error: 'Not found' }; } return { data: user }; });
fastify.post('/', { schema: { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' }, }, }, }, }, async (request, reply) => { const user = await fastify.db.user.create({ data: request.body }); reply.status(201); return { data: user }; });}
module.exports = userRoutes;Хуки (Hooks)
Заголовок раздела «Хуки (Hooks)»// Хуки — аналог middleware, но более структурированные
// onRequest — первый, для авторизацииapp.addHook('onRequest', async (request, reply) => { request.startTime = Date.now();});
// preValidation — до валидацииapp.addHook('preValidation', async (request, reply) => { // Проверяем токен const token = request.headers.authorization?.slice(7); if (!token) { reply.status(401).send({ error: 'Unauthorized' }); return; } request.user = jwt.verify(token, secret);});
// preHandler — после валидации, до обработчикаapp.addHook('preHandler', async (request, reply) => { app.log.info({ userId: request.user?.id, url: request.url });});
// onSend — перед отправкой ответаapp.addHook('onSend', async (request, reply, payload) => { // Можно модифицировать ответ return payload;});
// onResponse — после отправки ответа (для логирования)app.addHook('onResponse', async (request, reply) => { const duration = Date.now() - request.startTime; app.log.info(`${request.method} ${request.url} ${reply.statusCode} ${duration}ms`);});
// onError — при ошибкеapp.setErrorHandler(async (error, request, reply) => { app.log.error(error); reply.status(error.statusCode || 500).send({ error: error.message, statusCode: error.statusCode || 500, });});Декораторы
Заголовок раздела «Декораторы»// Добавить свойство к requestapp.decorateRequest('user', null);
// Добавить метод к replyapp.decorateReply('success', function (data) { this.status(200).send({ success: true, data });});
app.decorateReply('fail', function (message, status = 400) { this.status(status).send({ success: false, error: message });});
// Использованиеapp.get('/profile', async (request, reply) => { if (!request.user) return reply.fail('Unauthorized', 401); return reply.success(request.user);});Миграция с Express
Заголовок раздела «Миграция с Express»// Express → Fastify: основные отличия
// Express:app.use(middleware);app.get('/path', (req, res) => { res.json(data); });
// Fastify:app.register(plugin);app.addHook('onRequest', hook);app.get('/path', async (request, reply) => { return data; });
// req.body → request.body// req.params → request.params// req.query → request.query// res.json() → return data (или reply.send(data))// res.status(201).json() → reply.status(201); return data;// app.use(cors()) → app.register(require('@fastify/cors'))Практика
Заголовок раздела «Практика»- Создай Fastify сервер с JSON Schema валидацией для POST /users
- Реализуй плагин для подключения к БД
- Структурируй роуты через
app.register(routes, { prefix: '/api' }) - Добавь хук
onRequestдля логирования - Создай кастомный error handler через
setErrorHandler