DEV Community

Cover image for Validações com Yup + Swagger
Vitor Silva Delfino
Vitor Silva Delfino

Posted on

Validações com Yup + Swagger

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
}
...
Enter fullscreen mode Exit fullscreen mode

Alt Text

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)"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

Agora é só startar a aplicação e acessar a documentação

http://localhost:3000/api/docs

Alt Text

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,
};

Enter fullscreen mode Exit fullscreen mode

Agora se atualizarmos a página vemos os endpoints

Alt Text

E todos os requests podem ser feitos diretamente por ali

Alt Text

Alt Text

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)

Collapse
 
eduardoklosowski profile image
Eduardo Klosowski

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.

Collapse
 
vitordelfino profile image
Vitor Silva Delfino

Exatamente Eduardo

Collapse
 
vitorcalvi profile image
Vitor Calvi

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>'.

Collapse
 
vitorcalvi profile image
Vitor Calvi

Achei o erro, não sei se está correto.
No validator.ts
import { Request, Response, NextFunction } from 'express';

Collapse
 
vitorcalvi profile image
Vitor Calvi

Vitor, no arquivo validator.ts,
export const validateUserPayload = async (
req: Request,
_: Response,
next: NextFunction ...

Dá o erro de "Cannot find name NextFunction"

Collapse
 
vitorcalvi profile image
Vitor Calvi

Achei o erro, precisa importar:
import { NextFunction } from 'express';