Fastify usa uma abordagem baseada em esquema e, mesmo que não seja obrigatório, recomendamos o uso de JSON Schema para validar suas rotas e serializar suas saídas. Internamente, o Fastify compila o esquema em uma função de alto desempenho.
Trate a definição do esquema como código do aplicativo. Como os recursos de validação e serialização avaliam dinamicamente código com
new Function(), não é seguro usar esquemas fornecidos pelo usuário. Veja Ajv e fast-json-stringify para obter mais detalhes.
A validação de rota depende internamente do Ajv, que é um validador de esquema JSON de alto desempenho. A validação da entrada é muito fácil: basta adicionar os campos que você precisa dentro do esquema de rota e pronto! As validações suportadas são:
body: valida o corpo da solicitação se for um POST ou PUT.querystringouquery: valida a string de consulta. Pode ser um objeto JSON Schema completo (com uma propriedadetypede'object'e um objeto'properties'contendo parâmetros) ou uma variação mais simples na qual os atributostypeepropertiessão perdidos e os parâmetros da consulta estão listados no nível superior (veja o exemplo abaixo).params: valida os parâmetros da rota.headers: valida os cabeçalhos da solicitação.
Exemplo:
const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' }
},
nullableKey: { type: ['number', 'null'] }, // ou { type: 'number', nullable: true }
multipleTypesKey: { type: ['boolean', 'number'] },
multipleRestrictedTypesKey: {
oneOf: [
{ type: 'string', maxLength: 5 },
{ type: 'number', minimum: 10 }
]
},
enumKey: {
type: 'string',
enum: ['John', 'Foo']
},
notTypeKey: {
not: { type: 'array' }
}
}
}
const queryStringJsonSchema = {
name: { type: 'string' },
excitement: { type: 'integer' }
}
const paramsJsonSchema = {
type: 'object',
properties: {
par1: { type: 'string' },
par2: { type: 'number' }
}
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
fastify.post('/the/url', { schema }, handler)Observe que o Ajv tentará coagir os valores para os tipos especificados nas palavras-chave type do seu esquema, para passar a validação use o tipo primitivo correto.
Graças à API addSchema, você pode adicionar vários esquemas à instância Fastify e reutilizá-los em várias partes do seu aplicativo. Como sempre, essa API é encapsulada.
Há duas maneiras de reutilizar seus esquemas compartilhados:
$ref-way: conforme descrito em standard, você pode consultar um esquema externo. Para usá-lo, você precisaaddSchemacom um URI absoluto válido para$id.replace-way: este é um utilitário Fastify que permite substituir alguns campos por um esquema compartilhado. Para usá-lo, você precisaaddSchemacom um$idcom um fragmento de URI relativo, que é uma string simples que aplica-se apenas a caracteres alfanuméricos[A-Za-z0-9].
Aqui está uma visão geral sobre como definir um $id e como referenciá-lo:
replace-waymyField: 'foobar#'Irá procurar por um esquema compartilhado adicionado com$id: 'foobar'
$ref-waymyField: { $ref: '#foo'}Irá procurar por um campo com$id: '#foo'dentro do esquema atualmyField: { $ref: '#/definitions/foo'}Irá procurar por um campodefinitions.foodentro do esquema atualmyField: { $ref: 'http://url.com/sh.json#'}Irá procurar por um esquema compartilhado adicionado com$id: 'http://url.com/sh.json'myField: { $ref: 'http://url.com/sh.json#/definitions/foo'}Irá procurar por um esquema compartilhado adicionado com$id: 'http://url.com/sh.json'e usar o campodefinitions.foomyField: { $ref: 'http://url.com/sh.json#foo'}Irá procurar por um esquema compartilhado adicionado com$id: 'http://url.com/sh.json'e vai olhar dentro para um objeto com$id: '#foo'
Mais exemplos:
$ref-way Exemplo de uso:
fastify.addSchema({
$id: 'http://example.com/common.json',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
type: 'array',
items: { $ref: 'http://example.com/common.json#/properties/hello' }
}
},
handler: () => {}
})replace-way Exemplo de uso:
const fastify = require('fastify')()
fastify.addSchema({
$id: 'greetings',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: 'greetings#'
},
handler: () => {}
})
fastify.register((instance, opts, done) => {
/**
* No escopo filho, pode-se usar esquemas definidos no escopo pai, como 'greetings'.
* O escopo pai não pode usar os esquemas filhos.
*/
instance.addSchema({
$id: 'framework',
type: 'object',
properties: {
fastest: { type: 'string' },
hi: 'greetings#'
}
})
instance.route({
method: 'POST',
url: '/sub',
schema: {
body: 'framework#'
},
handler: () => {}
})
done()
})Você pode usar o esquema compartilhado em qualquer lugar, como esquema de nível superior ou aninhado dentro de outros esquemas:
const fastify = require('fastify')()
fastify.addSchema({
$id: 'greetings',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
type: 'object',
properties: {
greeting: 'greetings#',
timestamp: { type: 'number' }
}
}
},
handler: () => {}
})A função getSchemas retorna os esquemas compartilhados disponíveis no escopo selecionado:
fastify.addSchema({ $id: 'one', my: 'hello' })
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})Esse exemplo vai retornar:
| URL | Esquemas |
|---|---|
| / | one |
| /sub | one, two |
| /deep | one, two, three |
Você pode fornecer uma lista de plugins que você deseja utilizar com Ajv:
Referência a
ajv optionspara checar o formato dos plugins.
const fastify = require('fastify')({
ajv: {
plugins: [
require('ajv-merge-patch')
]
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
$patch: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: [
{
op: 'add',
path: '/properties/q',
value: { type: 'number' }
}
]
}
}
},
handler (req, reply) {
reply.send({ ok: 1 })
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
$merge: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: {
required: ['q']
}
}
}
},
handler (req, reply) {
reply.send({ ok: 1 })
}
})O schemaCompiler é uma função que retorna uma função que valida o corpo, os parâmetros de URL, os cabeçalhos e a string de consulta. O schemaCompiler padrão retorna uma função que implementa a interface de validação ajv. O Fastify o utiliza internamente para acelerar a validação.
Fastify's configuração base é:
{
removeAdditional: true, // remove propriedades adicionais
useDefaults: true, // substitui propriedades e itens ausentes pelos valores da palavra-chave padrão correspondente
coerceTypes: true, // alterar o tipo de dados para corresponder ao tipo de palavra-chave
allErrors: true, // checagem para todos os erros
nullable: true // suporte a palavra-chave "anulável" da especificação Open API 3.
}Essa configuração base pode ser modificada fornecendo [ajv.customOptions] (https://github.com/fastify/fastify/blob/main/docs/Server.md#factory-ajv) à sua fábrica do Fastify.
Se você deseja alterar ou definir opções de configuração adicionais, será necessário criar sua própria instância e substituir a existente, como:
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv({
// o padrão do fastify (se necessário)
removeAdditional: true,
useDefaults: true,
coerceTypes: true,
allErrors: true,
nullable: true,
// qualquer outra opção
// ...
})
fastify.setSchemaCompiler(function (schema) {
return ajv.compile(schema)
})
// -------
//Você definir um schema compiler usando a propriedade setter.
fastify.schemaCompiler = function (schema) { return ajv.compile(schema) })Nota: Se você usar uma instância personalizada de qualquer validador (até Ajv), precisará adicionar esquemas ao validador em vez de fastify, pois o validador padrão do fastify não será mais usado, e o método addSchema do fastify não terá idéia de qual validador você está usando.
A função schemaCompiler facilita a substituição do ajv por quase qualquer biblioteca de validação Javascript (joi, yup, ...).
No entanto, para fazer com que o mecanismo de validação escolhido funcione bem com o pipeline de solicitação/resposta do Fastify, a função retornada pela função schemaCompiler deve retornar um objeto com:
- em caso de falha na validação: uma propriedade
error, preenchida com uma instância deErrorou uma string que descreve o erro de validação - em caso de êxito da validação: uma propriedade
value, preenchida com o valor coagido que passou na validação
Os exemplos abaixo são, portanto, equivalentes:
const joi = require('joi')
// Opções de validação para corresponder às opções da base do ajv usadas no Fastify
const joiOptions = {
abortEarly: false, // retorna todos erros
convert: true, // alterar o tipo de dados para corresponder ao tipo de palavra-chave
allowUnknown : false, // remove propriedades adicionais
noDefaults: false
}
const joiBodySchema = joi.object().keys({
age: joi.number().integer().required(),
sub: joi.object().keys({
name: joi.string().required()
}).required()
})
const joiSchemaCompiler = schema => data => {
// A função joi `validate` retorna um objeto com uma propriedade de erro (se a validação falhar) e uma propriedade de valor (sempre presente, valor coagido se a validação for bem-sucedida)
const { error, value } = joiSchema.validate(data, joiOptions)
if (error) {
return { error }
} else {
return { value }
}
}
// ou mais simples...
const joiSchemaCompiler = schema => data => joiSchema.validate(data, joiOptions)
fastify.post('/the/url', {
schema: {
body: joiBodySchema
},
schemaCompiler: joiSchemaCompiler
}, handler)const yup = require('yup')
// Opções de validação para corresponder às opções base do ajv usadas no Fastify
const yupOptions = {
strict: false,
abortEarly: false, // retorna todos erros
stripUnknown: true, // remove propriedades adicionais
recursive: true
}
const yupBodySchema = yup.object({
age: yup.number().integer().required(),
sub: yup.object().shape({
name: yup.string().required()
}).required()
})
const yupSchemaCompiler = schema => data => {
// com a opção strict = false, a função yup `validateSync` retorna o valor coagido se a validação foi bem-sucedida ou lança se a validação falhou
try {
const result = schema.validateSync(data, yupOptions)
return { value: result }
} catch (e) {
return { error: e }
}
}
fastify.post('/the/url', {
schema: {
body: yupBodySchema
},
schemaCompiler: yupSchemaCompiler
}, handler)As mensagens de erro de validação do Fastify estão fortemente acopladas ao mecanismo de validação padrão: os erros retornados do ajv acabam sendo executados através da função schemaErrorsText, responsável pela criação de mensagens de erro amigáveis ao ser humano. No entanto, a função schemaErrorsText é escrita com ajv em mente: como resultado, você pode receber mensagens de erro estranhas ou incompletas ao usar outras bibliotecas de validação.
Para contornar esse problema, você tem 2 opções principais:
- certifique-se de que sua função de validação (retornada pelo seu
schemaCompilerpersonalizado) retorne erros exatamente na mesma estrutura e formato que oajv(embora isso possa ser difícil e complicado devido a diferenças entre os mecanismos de validação) - ou use um
errorHandlerpersonalizado para interceptar e formatar seus erros de validação 'personalizados'
Para ajudá-lo a escrever um errorHandler personalizado, o Fastify adiciona 2 propriedades a todos os erros de validação:
- validation: o conteúdo da propriedade
errordo objeto retornado pela função de validação (retornada pelo seuschemaCompilerpersonalizado) - validationContext: o 'contexto' (corpo, parâmetros, consulta, cabeçalhos) em que ocorreu o erro de validação
Um exemplo muito bem elaborado de um errorHandler personalizado que manipula erros de validação é mostrado abaixo:
const errorHandler = (error, request, reply) => {
const statusCode = error.statusCode
let response
const { validation, validationContext } = error
// verifica se houve um erro de validação
if (validation) {
response = {
mensagem: `Ocorreu um erro de validação ao validar o ${validationContext}...`, // validationContext será 'body', 'params', 'headers' ou 'query'
errors: validation // este é o resultado da sua biblioteca de validação...
}
} else {
response = {
message: 'Um erro ocorreu...'
}
}
// qualquer trabalho adicional aqui, por exemplo: log de erro
// ...
reply.status(statusCode).send(response)
}O schemaResolver é uma função que trabalha junto com o schemaCompiler: você não pode usá-la
com o compilador de esquema padrão. Esse recurso é útil quando você usa esquemas complexos com a palavra-chave $ref
em suas rotas e um validador personalizado.
Isso é necessário porque todos os esquemas adicionados ao seu compilador personalizado são desconhecidos para o Fastify, mas
precisa resolver os caminhos $ref.
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv()
ajv.addSchema({
$id: 'urn:schema:foo',
definitions: {
foo: { type: 'string' }
},
type: 'object',
properties: {
foo: { $ref: '#/definitions/foo' }
}
})
ajv.addSchema({
$id: 'urn:schema:response',
type: 'object',
required: ['foo'],
properties: {
foo: { $ref: 'urn:schema:foo#/definitions/foo' }
}
})
ajv.addSchema({
$id: 'urn:schema:request',
type: 'object',
required: ['foo'],
properties: {
foo: { $ref: 'urn:schema:foo#/definitions/foo' }
}
})
fastify.setSchemaCompiler(schema => ajv.compile(schema))
fastify.setSchemaResolver((ref) => {
return ajv.getSchema(ref).schema
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: ajv.getSchema('urn:schema:request').schema,
response: {
'2xx': ajv.getSchema('urn:schema:response').schema
}
},
handler (req, reply) {
reply.send({ foo: 'bar' })
}
})Normalmente, você envia seus dados para os clientes via JSON, e o Fastify possui uma ferramenta poderosa para ajudá-lo, fast-json-stringify que será usado se você tiver fornecido um esquema de saída nas opções de rota. Recomendamos que você use um esquema de saída, pois aumentará sua taxa de transferência de 100 a 400%, dependendo da carga útil, e evitará a divulgação acidental de informações confidenciais.
Example:
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)Como você pode ver, o esquema de resposta é baseado no código de status. Se você deseja usar o mesmo esquema para vários códigos de status, pode usar '2xx', por exemplo:
const schema = {
response: {
'2xx': {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
},
201: {
type: 'object',
properties: {
value: { type: 'string' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)Se você precisar de um serializador personalizado em uma parte muito específica do seu código, poderá configurá-lo com reply.serializer (...).
Quando a validação do esquema falha em uma solicitação, o Fastify retorna automaticamente uma resposta de status 400, incluindo o resultado do validador na carga útil. Como exemplo, se você tiver o seguinte esquema para sua rota
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}e não satisfazê-lo, a rota retornará imediatamente uma resposta com a seguinte carga útil
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}Se você deseja lidar com erros dentro da rota, pode especificar a opção attachValidation para sua rota. Se houver um erro de validação, a propriedade validationError da solicitação conterá o objeto Error com o resultado bruto de validation, como mostrado abaixo
const fastify = Fastify()
fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
if (req.validationError) {
// `req.validationError.validation` contém o erro bruto de validação
reply.code(400).send(req.validationError)
}
})Você também pode usar setErrorHandler para definir uma resposta personalizada para erros de validação, como:
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
// error.validationContext pode ser um de [body, params, querystring, headers]
reply.status(422).send(new Error(`Erro de validação: ${error.validationContext}`))
}
})Se você deseja uma resposta de erro personalizada no esquema sem dores de cabeça e rapidamente, consulte aqui
O esquema JSON possui algum tipo de utilitário para otimizar seus esquemas que, em conjunto com o esquema compartilhado do Fastify, permite reutilizar todos os seus esquemas facilmente.
| Caso de uso | Validator | Serializer |
|---|---|---|
| Esquema compartilhado | ✔️ | ✔️ |
$ref para $id |
✔️ | ✔️ |
$ref para /definitions |
✔️ | ✔️ |
$ref para esquema compartilhado $id |
✔️ | ✔️ |
$ref para esquema compartilhado /definitions |
✔️ | ✔️ |
// Uso do recurso Esquema Compartilhado
fastify.addSchema({
$id: 'sharedAddress',
type: 'object',
properties: {
city: { 'type': 'string' }
}
})
const sharedSchema = {
type: 'object',
properties: {
home: 'sharedAddress#',
work: 'sharedAddress#'
}
}// Uso de $ref para $id no mesmo esquema JSON
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}// Uso de $ref para /definitions no mesmo esquema JSON
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}// Uso de $ref para um esquema compartilhado $id como esquema externo
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
}
})
const refToSharedSchemaId = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#address' },
work: { $ref: 'http://foo/common.json#address' }
}
}// Usa $ref para um esquema compartilhado /definições como um schema externo
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
}
})
const refToSharedSchemaDefinitions = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#/definitions/foo' },
work: { $ref: 'http://foo/common.json#/definitions/foo' }
}
}- JSON Schema
- Entendendo JSON schema
- fast-json-stringify documentação
- Ajv documentação
- Ajv i18n
- Ajv custom errors
- Manipulação de erro personalizada com métodos principais com dumping de arquivo de erro exemplo