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
Top comments (6)
Usar e manter o swagger pode ser bem trabalhoso, mas ao se acostumar com ele fica muito mais fácil ter uma visão de como montar boas APIs, visualizando o comportamento de uma URL antes mesmo de começar a escrever o código, e verificar o que e como está sendo exposto para outras aplicações. Com certeza vale o esforço de fazer pelo menos uma vez.
Exatamente Eduardo
Vitor, no src/apps/Users/routes.ts
route.post('/', validateUserPayload, controller.create);
Dá o seguinte erro:
No overload matches this call.
The last overload gave the following error.
Argument of type '(req: Request, _: Response, next: NextFunction) => Promise' is not assignable to parameter of type 'RequestHandlerParams>'.
Type '(req: Request, _: Response, next: NextFunction) => Promise' is not assignable to type 'RequestHandler>'.
Achei o erro, não sei se está correto.
No validator.ts
import { Request, Response, NextFunction } from 'express';
Vitor, no arquivo validator.ts,
export const validateUserPayload = async (
req: Request,
_: Response,
next: NextFunction ...
Dá o erro de "Cannot find name NextFunction"
Achei o erro, precisa importar:
import { NextFunction } from 'express';