OpenAPI спецификация содержит описание структур данных в свойстве components
в виде JSON Schema. Она описывает структуру входных и выходных данных включая: имена полей и их обязательность, типы полей и правила валидации.
components:
schemas:
User:
type: object
required:
- id
- fullName
- email
- createdAt
properties:
id:
type: number
fullName:
type: string
nullable: true
minLength: 2
maxLength: 100
email:
type: string
format: email
createdAt:
type: string
format: date-time
JSON Schema позволяет реализовать несколько полезных вещей:
- Получить готовые структуры, например классы и интерфейсы в языках где они поддерживаются, например в TypeScript. Это дает поддержку редактора для автокомплита и подсветки ошибок.
- Автоматизировать процесс проверки входных и выходных данных в рантайме, с возвратом нужного кода ответа.
Валидация в Fastify
Fastify поддерживает валидацию на базе JSON Schema из коробки. Поддерживаемые валидации включают:
- body: Тело запроса.
- querystring: Строка запроса.
- params: Параметры запроса, например /users/:id (id параметр).
- headers: Заголовки запроса.
- response: Тело ответа с учетом разных кодов и форматов.
Пример:
const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' },
},
},
}
const queryStringJsonSchema = {
type: 'object',
properties: {
page: { type: 'integer' },
direction: { type: 'string' },
},
}
const paramsJsonSchema = {
type: 'object',
properties: {
id: { type: 'integer' },
},
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' },
},
required: ['x-foo'],
}
const responseJsonSchema = {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' },
},
},
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema,
response: responseJsonSchema,
}
fastify.post('/users', { schema }, handler)
Fastify не только сверяет совпадение по структуре и типами, но так же выполняет преобразования типов. По умолчанию все входящие данные являются строкой, но если, например, в схеме указано, что параметр page
это число, то значение этого параметра будет преобразовано в число. Ручных преобразований при таком подходе не будет вообще.
fastify.get(
'/users',
{
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer' },
},
},
},
},
async function (request) {
// page преобразуется в number автоматически
const { page = 1 } = request.query
const users = await db.query
.users
.findMany({
orderBy: asc(schemas.users.id),
...getPagingOptions(page, 1),
})
return users
},
)
Когда структуры не совпадают, то Fastify автоматически обрабатывает ошибки, возвращая код 400.
npm run dev
> js-fastify-rest-api-example@1.0.0 dev
> fastify start -w -l info -P app.js
[12:53:46.390] INFO (29532): Server listening at http://[::1]:3000
[12:53:46.390] INFO (29532): Server listening at http://127.0.0.1:3000
curl localhost:3000/api/users
[{"id":1,"fullName":"Ms. Carol Boyle","email":"Kenton_Funk@yahoo.com","updatedAt":null,"createdAt":"1726592026"}]
curl "localhost:3000/api/users?page=str"
{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"querystring/page Expected number"}
Валидация на базе OpenAPI
Благодаря подходу API First, у нас уже есть все необходимые схемы в файле openapi.json, сгенерированным TypeSpec. В теории мы могли бы прочитать этот файл внутри Fastify и передать схемы в каждый обработчик. На практике, это будет неудобно из-за указания на связи $ref
, которые автоматически не раскрываются. Придется много делать много дополнительных движений.
paths:
/users:
get:
operationId: users_index
parameters:
- name: page
in: query
required: false
schema:
type: number
default: 1
explode: false
responses:
'200':
description: The request has succeeded.
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
required:
- data
Но есть и более серьезная проблема. При использовании TypeScript, передача схем напрямую не формирует нужные типы. Это значит, что работая внутри обработчика мы постоянно будем натыкаться на ошибки при обращении к полям входных данных о том, что они не существуют. Эта проблема актуальна и для JavaScript.
TypeBox
Чтобы добиться нужного эффекта с преобразованием схемы в типы, нужно добавить в процесс новый элемент, который называется TypeBox. Эта библиотека представляет собой язык описания JSON Schema на JavaScript/TypeScript.
import { Type } from '@sinclair/typebox'
const T = Type.Object({ // const T = {
x: Type.Number(), // type: 'object',
y: Type.Number(), // required: ['x', 'y', 'z'],
z: Type.Number(), // properties: {
}) // x: { type: 'number' },
// y: { type: 'number' },
// z: { type: 'number' }
// }
// }
Но работать с ней напрямую нам не нужно. Процесс будет выглядеть так:
- Указываем Fastify, что мы работаем не с JSON Schema, а TypeBox.
- С помощью библиотеки генерируем TypeBox схему на основе OpenAPI спецификации.
- Подключаем TypeBox схему к обработчикам, вместо прямого описания JSON Schema
Установка и настройка
Установим пакеты для совместной работы Fastify и Typebox
npm i @fastify/type-provider-typebox @sinclair/typebox
Укажем Fastify на необходимость использовать TypeBox. Внутри файла app.js поменяем объект
fastify
на объектapi
и заменим использование одного на другойimport { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox' export default fp(async function (fastify, opts) { const api = fastify .setValidatorCompiler(TypeBoxValidatorCompiler) .withTypeProvider() // В коде ниже нужно заменить использование fastify на api }
Генерация схемы
Схема генерируется командой openapi-box
:
npx openapi-box ./tsp-output/@typespec/openapi3/openapi.json
Результирующий файл называется schema.js и располагается в корне проекта. Внутри него описано множество подобных определений:
const ComponentsSchemasUser = T.Object({
id: T.Number(),
fullName: T.Union([T.Null(), T.String()]),
email: T.String(),
createdAt: T.String({ format: 'date-time' }),
})
Для удобства имеет смысл объединить команды компиляции tsp
и генерации схемы openapi-box
в одну команду, например с помощью Makefile:
types-to-openapi:
npx tsp compile .
types-to-typebox:
npx openapi-box ./tsp-output/@typespec/openapi3/openapi.json
types: types-to-openapi types-to-typebox
В таком случае для генерации всех нужных файлов будет достаточно набрать make types
:
make types
npx tsp compile .
TypeSpec compiler v0.59.1
Compilation completed successfully.
npx openapi-box ./tsp-output/@typespec/openapi3/openapi.json
✔ Schema done!
Использование схемы
Для подключения готовой схемы, ее нужно импортировать в нужный файл с маршрутами и добавить в обработчики.
import { schema } from '../../schema.js'
/**
* @param {import('fastify').FastifyTypebox} fastify
*/
export default async function (fastify) {
fastify.get(
'/users',
{
schema: {
querystring: schema['/users'].GET.args.properties.query,
},
},
async (request) => {
// Код
},
)
fastify.get(
'/users/:id',
{
schema: schema['/users/{id}'].GET.args.properties,
},
async (request) => {
// Код
},
)
fastify.post(
'/users',
{
schema: {
body: schema['/users'].POST.args.properties.body,
response: {
201: schema['/users'].POST.data,
},
},
},
async (request, reply) => {
// Код
},
)
fastify.patch(
'/users/:id',
{
// Возвращает { params: ..., body: ... }
schema: schema['/users/{id}'].PATCH.args.properties,
},
async (request) => {
// Код
},
)
fastify.delete(
'/users/:id',
{
// Возвращает { params: ... }
schema: schema['/users/{id}'].DELETE.args.properties,
},
async (request, reply) => {
// Код
},
)
}
Импортируемый объект из файла schema.js имеет следующую структуру:
# Для возвращаемых данных
<маршрут>.<МЕТОД>.data
# Для входных данных
<маршрут>.<МЕТОД>.args.properties.<query|body|params>
К этой схеме нужно привыкнуть. Запоминать ее не нужно, проще всего она отрабатывается на практических примерах, в идеале через подсказки редактора, как это происходит с нашим примером на Github.
Самостоятельная работа
- Добавьте валидацию данных для всех роутов используя генерацию схем
- Запушьте изменения в репозиторий
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.