Сегодня разберёмся, в чём боль code-first, что меняет schema-first и как это выглядит в Fastify.
Code-first
Правила формы данных живут в коде, рассыпанные по разным местам. Валидация — отдельная функция. Типы — отдельный интерфейс. Обе сущности — копии одной и той же правды в разных синтаксисах.
Знакомая картина — ручная валидация формы:
function validateUserForm(values) {
const errors = {}
if (!values.email) errors.email = 'required'
if (!values.age || Number.isNaN(+values.age)) errors.age = 'must be number'
return errors
}
// где-то рядом
type UserForm = { email: string; age: number }
Два источника правды на одну сущность. Один обновил — другой забыл. Дальше прод, неприятный сюрприз, разбор полётов в Slack.
Schema-first
Подход, при котором описание формы данных живёт отдельно от бизнес-логики и является источником правды. Описываем форму декларативно — например, объектом JSON Schema. Дальше из этого объекта получаются:
— валидация входа (запрос проверяется по схеме до того, как попадёт в обработчик);
— сериализация выхода (ответ режется по схеме, лишние поля не уйдут наружу);
— TypeScript-типы (генерятся из схемы — например, через TypeBox или json-schema-to-ts).
Одна декларация — три артефакта. Обновил схему — всё остальное само догнало.
Как это в Fastify
Fastify построен вокруг schema-first из коробки. Минимальный пример:
const schema = {
body: {
type: 'object',
required: ['email', 'age'],
properties: {
email: { type: 'string', format: 'email' },
age: { type: 'integer' }
},
additionalProperties: false
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'integer' },
email: { type: 'string' }
}
}
}
}
fastify.post('/users', { schema }, async (req, reply) => {
const user = await db.users.insert(req.body)
reply.code(201).send(user)
})
Что мы получили бесплатно:
— request body валидируется до обработчика. Невалидный — 400 с понятным сообщением, обработчик даже не дёргается;
— response сериализуется по схеме. Если в user внезапно прилетело password_hash — он не уйдёт наружу. Это и про безопасность, и про предсказуемость контракта;
— additionalProperties: false режет лишние поля на входе. Прислали лишнее — до обработчика оно не доедет. Если хочется именно 400, это уже настраивается через Ajv.
Бонусом — скорость
Под капотом сериализация ответа идёт не через JSON.stringify, а через fast-json-stringify: на старте по response-схеме компилируется специализированная функция, и дальше она работает в разы быстрее (в бенчах самой библиотеки — до 2x на типовых ответах). JSON.stringify каждый раз обходит объект на ходу и не знает заранее, какие поля в нём окажутся. fast-json-stringify знает структуру и идёт почти прямым проходом по известным полям. Платим за это одной компиляцией на старте — копейки.
Дока Fastify.
Резюме
— schema-first даёт один источник правды — декларацию данных, из которой вытекают валидация, сериализация и типы;
— code-first проще на старте, но копии одной правды быстро расходятся, и это всегда вылазит больно;
— в Fastify это работает из коробки, плюс бонусом сериализация в разы быстрее обычного JSON.stringify.