Validações com Yup + Swagger
Vitor Silva Delfino
Posted on March 23, 2021
Dando continuidade na aplicação, vamos escrever um middleware para validação do payload recebido, e escrever a documentação da API utilizando Swagger.
Yup
Yup é um construtor de esquema JavaScript para análise e validação de valor
Instalações
Vamos instalar a lib e seus types.
yarn add yup@0.28.5 && yarn add -D @types/yup
Após a instalação, vamos configurar uma instância do Yup.
src/config/yup.ts
import * as yup from 'yup';
yup.setLocale({
string: {
email: 'Preencha um email válido',
min: '${path}: valor muito curto (mínimo ${min} caracteres)',
max: '${path}: valor muito longo (máximo ${max} caracteres)',
matches: '${path}: valor inválido, verifique o formato esperado',
length: '${path}: deve conter exatamente ${length} caracteres',
},
mixed: {
required: '${path} é um campo obrigatório',
oneOf: '${path} deve ser um dos seguintes valores [${values}]',
},
});
export default yup;
Importamos o yup e configuramos algumas mensagens padrão para cada tipo de validação feito.
Com yup configurado, vamos escrever uma validação para o nosso cadastro de usuário.
src/apps/Users/validator.ts
import yup from '@config/yup';
export const validateUserPayload = async (
req: Request,
_: Response,
next: NextFunction
): Promise<void> => {
await yup
.object()
.shape({
name: yup.string().required(),
document: yup.string().length(11).required(),
password: yup.string().min(6).max(10).required(),
})
.validate(req.body, { abortEarly: false });
return next();
};
Definimos algumas regras para o payload da criação de usuário
- name, document e password são obrigatórios
- document deve ter 11 caracteres
- password deve ter no mínimo 6 e no máximo 10 caracteres
E na rota, antes de passar a request para a controller, vamos adicionar o middleware de validação
src/apps/Users/routes.ts
import { Router } from 'express';
import * as controller from './UserController';
import { validateUserPayload } from './validator';
import 'express-async-errors';
const route = Router();
route.post('/', validateUserPayload, controller.create);
route.get('/:id', controller.findOne);
route.put('/:id', controller.update);
route.delete('/:id', controller.deleteOne);
export default route;
Vamos testar nossa validação.
No arquivo de requests, vamos adicionar um request com payload inválido e executa-lo.
...
POST http://localhost:3000/api/users HTTP/1.1
Content-Type: application/json
{
"name": "Vitor",
"document": "123",
"password": "1234"
}
...
A lib express-handlers-errors sabe lidar com os erros devolvidos pelo Yup. E podemos ver as mensagens de erro no retorno.
{
"errors": [
{
"code": "ValidationError",
"message": "document: deve conter exatamente 11 caracteres"
},
{
"code": "ValidationError",
"message": "password: valor muito curto (mínimo 6 caracteres)"
}
]
}
Swagger
Agora que já sabemos escrever validações com Yup, vamos documentar os endpoints da nossa aplicação.
Instalações
Começamos instalando a lib swagger-ui-express
yarn add swagger-ui-express && yarn add -D @types/swagger-ui-express
Após a instalação, vamos escrever um script.
Esse script vai ser executado sempre no start da aplicação, e vai varrer todas as pastas dentro de src/apps
procurando um arquivo swagger.ts
Então como convenção, cada módulo da aplicação terá um arquivo de documentação, por exemplo:
-
src/apps/Users/swagger.ts
aqui vai estar toda a documentação do módulo de usuário -
src/apps/Products/swagger.ts
aqui vai estar toda a documentação do módulo de produtos - ...
Vamos ao middleware:
src/middlewares/swagger.ts
import fs from 'fs';
import { resolve } from 'path';
class SwaggerConfig {
private readonly config: any;
private paths = {};
private definitions = {};
constructor() {
// Aqui fazemos uma configuração inicial, informando o nome da aplicação e definindo alguns tipos
this.config = {
swagger: '2.0',
basePath: '/api',
info: {
title: 'Tutorial de Node.JS',
version: '1.0.0',
},
schemes: ['http', 'https'],
consumes: ['application/json'],
produces: ['application/json'],
securityDefinitions: {
Bearer: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
},
},
};
this.definitions = {
ErrorResponse: {
type: 'object',
properties: {
errors: {
type: 'array',
items: {
$ref: '#/definitions/ErrorData',
},
},
},
},
ErrorData: {
type: 'object',
properties: {
code: {
type: 'integer',
description: 'Error code',
},
message: {
type: 'string',
description: 'Error message',
},
},
},
};
}
/**
* Função responsável por percorrer as pastas e adicionar a documentação de cada módulo
* @returns
*/
public async load(): Promise<{}> {
const dir = await fs.readdirSync(resolve(__dirname, '..', 'apps'));
const swaggerDocument = dir.reduce(
(total, path) => {
try {
const swagger = require(`../apps/${path}/swagger`);
const aux = total;
aux.paths = { ...total.paths, ...swagger.default.paths };
if (swagger.default.definitions) {
aux.definitions = {
...total.definitions,
...swagger.default.definitions,
};
}
return total;
} catch (e) {
return total;
}
},
{
...this.config,
paths: { ...this.paths },
definitions: { ...this.definitions },
}
);
return swaggerDocument;
}
}
export default new SwaggerConfig();
E então configuramos as rotas para apresentação da documentação:
src/swagger.routes.ts
import { Router, Request, Response } from 'express';
import { setup, serve } from 'swagger-ui-express';
import SwaggerDocument from '@middlewares/swagger';
class SwaggerRoutes {
async load(): Promise<Router> {
const swaggerRoute = Router();
const document = await SwaggerDocument.load();
swaggerRoute.use('/api/docs', serve);
swaggerRoute.get('/api/docs', setup(document));
swaggerRoute.get('/api/docs.json', (_: Request, res: Response) =>
res.json(document)
);
return swaggerRoute;
}
}
export default new SwaggerRoutes();
E nas configurações do express, usaremos essa rota
src/app.ts
...
import routes from './routes';
import swaggerRoutes from './swagger.routes';
import 'reflect-metadata';
class App {
public readonly app: Application;
private readonly session: Namespace;
constructor() {
this.app = express();
this.session = createNamespace('request'); // é aqui que vamos armazenar o id da request
this.middlewares();
this.configSwagger(); // Aqui chamamos a função para configurar o swagger
this.routes();
this.errorHandle();
}
...
private async configSwagger(): Promise<void> {
const swagger = await swaggerRoutes.load();
this.app.use(swagger);
}
...
export default new App();
Agora é só startar a aplicação e acessar a documentação
Configurando a documentação das rotas
Vamos escrever a documentação do nosso módulo de usuários
Em todo arquivo vamos exportar dois objetos, paths
e definitions
- em paths definimos as rotas
- em definitions definimos os modelos
Em qualquer caso de dúvida, é só acessar a documentação
src/apps/Users/swagger.ts
const paths = {
'/users/{id}': {
get: {
tags: ['User'],
summary: 'User',
description: 'Get user by Id',
security: [
{
Bearer: [],
},
],
parameters: [
{
in: 'path',
name: 'id',
required: true,
schema: {
type: 'string',
},
description: 'uuid',
},
],
responses: {
200: {
description: 'OK',
schema: {
$ref: '#/definitions/User',
},
},
404: {
description: 'Not Found',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
500: {
description: 'Internal Server Error',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
},
},
put: {
tags: ['User'],
summary: 'User',
description: 'Update user',
security: [
{
Bearer: [],
},
],
parameters: [
{
in: 'path',
name: 'id',
required: true,
schema: {
type: 'string',
},
description: 'uuid',
},
{
in: 'body',
name: 'update',
required: true,
schema: {
$ref: '#/definitions/UserPayload',
},
},
],
responses: {
200: {
description: 'OK',
schema: {
$ref: '#/definitions/User',
},
},
404: {
description: 'Not Found',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
500: {
description: 'Internal Server Error',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
},
},
delete: {
tags: ['User'],
summary: 'User',
description: 'Delete User',
security: [
{
Bearer: [],
},
],
parameters: [
{
in: 'path',
name: 'id',
required: true,
schema: {
type: 'string',
},
description: 'uuid',
},
],
responses: {
200: {
description: 'OK',
},
404: {
description: 'Not Found',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
500: {
description: 'Internal Server Error',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
},
},
},
'/users': {
post: {
tags: ['User'],
summary: 'User',
description: 'Create user',
security: [
{
Bearer: [],
},
],
parameters: [
{
in: 'body',
name: 'update',
required: true,
schema: {
$ref: '#/definitions/UserPayload',
},
},
],
responses: {
200: {
description: 'OK',
schema: {
$ref: '#/definitions/User',
},
},
404: {
description: 'Not Found',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
500: {
description: 'Internal Server Error',
schema: {
$ref: '#/definitions/ErrorResponse',
},
},
},
},
},
};
const definitions = {
User: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
document: { type: 'string' },
password: { type: 'string' },
createdAt: { type: 'date' },
updatedAt: { type: 'date' },
},
},
UserPayload: {
type: 'object',
properties: {
name: { type: 'string' },
document: { type: 'string' },
password: { type: 'string' },
},
},
};
export default {
paths,
definitions,
};
Agora se atualizarmos a página vemos os endpoints
E todos os requests podem ser feitos diretamente por ali
Considerações finais
Documentar a api com swagger é realmente muito verboso, e a cada mudança nas internfaces/contratos o swagger deve ser atualizado.
Mas mantendo a documentação em dia, você facilita o trabalho do QA, do front que vai realizar a integração e muito mais.
O que está por vir
No próximo post, vamos configurar o jest e implementar o primeiro teste unitário. E para simular um teste sem precisa acessar a base de dados, vamos mockar as funções do typeorm
Posted on March 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.