Validações com Yup + Swagger
Tags: node, braziliandevs, typescript, javascript
Published: 2021-03-23 21:36:12

Dando continuidade na aplicação, vamos escrever um middleware para validação do payload recebido, e escrever a documentação da API utilizando [Swagger](https://swagger.io).

# Yup


> *[Yup](https://www.npmjs.com/package/yup) é um construtor de esquema JavaScript para análise e validação de valor*


## Instalações


Vamos instalar a lib e seus types.

```bash
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`
```typescript
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`
```typescript
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`
```typescript
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.

```http
...
POST http://localhost:3000/api/users HTTP/1.1
Content-Type: application/json

{
  "name": "Vitor",
  "document": "123",
  "password": "1234"
}
...
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8rhcxousbbcclhxyo0l.png)

A lib [express-handlers-errors](https://www.npmjs.com/package/express-handler-errors) sabe lidar com os erros devolvidos pelo Yup. E podemos ver as mensagens de erro no retorno.

```json
{
  "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](https://www.npmjs.com/package/swagger-ui-express)

```bash
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`
```typescript
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`
```typescript
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`
```typescript
...

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

> http://localhost:3000/api/docs

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mv5m53bnb2e314e1bx0p.png)


## 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](https://swagger.io/docs/specification/2-0/basic-structure/)

`src/apps/Users/swagger.ts`
```typescript
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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnkf3d0q61jej9m7x9mn.png)

E todos os requests podem ser feitos diretamente por ali

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ayv1b4vdgobp1t4y61sj.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ec5ocpq3w5xbkf66rz7.png)

# 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](https://jestjs.io/pt-BR/) e implementar o primeiro teste unitário. E para simular um teste sem precisa acessar a base de dados, vamos [mockar ](https://pt.wikipedia.org/wiki/Objeto_mock) as funções do typeorm