Hey dev, usando Zod no Angular? Pra que?

No mundo do desenvolvimento javascript, a biblioteca Zod é uma poderosíssima ferramenta para validação de dados e formularios. Não seria nada legal salvar um NaN ou undefined no banco de dados né? né????

Pois bem, temos no mundo do React o costume de usar bibliotecas para tudo (afinal, React é uma casca vazia), e uma delas é o poderoso Zod para validação de formularios.

Observe o exemplo:


import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Definindo o esquema de validação com Zod
const schema = z.object({
  nome: z.string().min(2, 'Muito curto').max(50, 'Muito longo'),
  sobrenome: z.string().min(2, 'Muito curto').max(50, 'Muito longo'),
  senha: z.string().min(8, 'A senha deve ter pelo menos 8 caracteres'),
  repetirSenha: z.string().refine((value) => value === senha, { message: 'As senhas precisam ser iguais' }),
});

export const Formulario = () => {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Nome:</label>
        <input {...register("nome")} />
        {errors.nome && <p>{errors.nome.message}</p>}
      </div>
      <div>
        <label>Sobrenome:</label>
        <input {...register("sobrenome")} />
        {errors.sobrenome && <p>{errors.sobrenome.message}</p>}
      </div>
      <div>
        <label>Senha:</label>
        <input type="password" {...register("senha")} />
        {errors.senha && <p>{errors.senha.message}</p>}
      </div>
      <div>
        <label>Repetir Senha:</label>
        <input type="password" {...register("repetirSenha")} />
        {errors.repetirSenha && <p>{errors.repetirSenha.message}</p>}
      </div>
      <button type="submit">Enviar</button>
    </form>
  );
};

Podemos ter a mesma validação no backend na hora de receber um body http e vários outros exemplos. Mas porque citei o React? Porque o assunto está caminhando para o framework frontend Angular.

Então vamos lá, vamos criar um componente Angular independente usando um template string para montar o nosso html.


@Component({
  selector: 'app-formulario',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule], //importando o ReactiveFormsModule no componente
  template: `
    <form
        (submit)="enviarFormulario"
        [formGroup]="form" // injetando o atributo form no formulario
        *ngIf="form" // Utilizando o ngIf aqui para evitar erro ExpressionChangedAfterItHasBeenCheckedError
    >
      <div>
        <label>Nome:</label>
        <input type="text"
        formControlName="nome" //Passando aqui um controle reativo no formulario
        />
      </div>
      <div>
        <label>Sobrenome:</label>
        <input type="text" formControlName="sobrenome" />
      </div>
      <div>
        <label>Senha:</label>
        <input type="password" formControlName="senha"  />
      </div>
      <div>
        <label>Repetir Senha:</label>
        <input type="password" formControlName="repetirSenha" />
      </div>
      <button type="submit">Enviar</button>
    </form>
  `,
})
export class FormularioComponent {
    public form!: FormGroup;
}

Ótimo, agora, pro nosso componente não fica muito extenso, vamos trabalhar dentro da classe omitindo o html. Mas lembre-se da regra dos decoratos senhor copia e cola que caiu aqui no desespero, decorator tem que ficar acima de uma classe.

Vamos começar instanciando o nosso grupo de formulario usando a classe FormGroup. Dentro do objeto, vamos usar um objeto onde os valores são instancias de FormControl. Ficou dificil? Vamos tirar a prova no código!

obs: instanciamos os elementos no hook onInit

@Component({...})
export class FormularioComponent {
    public form!: FormGroup;
    
    ngOnInit(): void {
        this.form = new FormGroup({
          name: new FormControl('', [Validators.required, Validators.minLength(2)]),
          sobrenome: new FormControl('', [Validators.required, Validators.maxLength(50)]),
          senha: new FormControl('', [Validators.minLength(8), Validators.pattern(Regex)]),
          repetirSenha: new FormControl('', [Validators.required, Validators.minLength(8)]),
        })
    }
}

Pronto! Agora nos temos um formulario reativo que acrecenta em cada atributo o input do formulário de forma reativa. Usamos também a classe estatica Validators para usar validação nos campos de formulário. Para encerar, vamos criar uma validação personalizada para verificar se o campo senha e repetirSenha são iguais.

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export const MustMatch = (value: string, valueMatch: string): ValidatorFn | null => {
  return (control: AbstractControl): ValidationErrors | null => {
    const match = control.get(value);
    const valueToMatch = control.get(valueMatch);

    if (match?.value !== valueToMatch?.value) {
      valueToMatch?.setErrors({ mustMatch: true });
      return { mustMatch: true };
    }

    return null;
  };
};

Agora vamos implementar no componente utilizando como segundo argumento de FormGroup.

@Component({...})
export class FormularioComponent {
    
    ngOnInit(): void {
        this.form = new FormGroup({...}, { validators: MustMatch('senha', 'repetirSenha') })
    }
}

Ótimo, agora podemos acessar diretamente os erros de cada campo diretamente no html usando os getters da instancia de FormGroup.


<div *ngIf="form.get('password')?.invalid && form.get('senha')?.touched">
    <div *ngIf="form.get('senha')?.errors?.['required']">Senha não pode ficar em branco</div>
    <div *ngIf="form.get('senha')?.errors?.['minlength']">Senha muita curta</div>
    <div *ngIf="form.get('senha')?.errors?.['mustMatch']">Senhas não são iguais </div>
    <div *ngIf="form.get('senha')?.errors?.['pattern']">Senha não atende os requisitos </div>
</div>

É isso. O Angular faz o resto. E não precisamos instalar uma nova dependencia para isso.

Fale comigo na comunidade que participo

No Angular não é bom usar o Zod para validação de formulário justamente porque ali já temos meios nativos de se fazer isso.

Mas isso não quer dizer que o Zod seja descartável em um projeto Angular.

O Zod brilha em lugares como, por exemplo, respostas de APIs. Imagina que sua app espera receber um JSON assim:

{ "id": 1, "name": "Fulano da Silva", "active": 2 }

E vc constrói toda sua lógica esperando receber um ID numérico e uma propriedade active com os valores 2 para "ativo" e 1 para "inativo".

type User = { id: number; name: string; active: 1 | 2 };

@Injectable({ providedIn: "root" })
export class UserService {
  getAll(): Observable<User[]> {
    // Faz de conta que tá vindo do HttpClient 😉
    return of([
      { id: 1, name: "João", active: 2 },
      { id: 2, name: "Maria", active: 1 },
    ]);
  }
}

Num belo dia, os devs da API decidem que agora a propriedade active deve ser um booleano e que o ID seria um UUID. Pronto! Sua lógica pode falhar.

Em algum momento vc vai perceber que a falha não vem da sua app, mas da resposta da API, mas com o Zod vc descobriria logo de cara onde o problema vem.

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  active: z.union([z.literal(1), z.literal(2)]),
});

type User = z.infer<typeof userSchema>;

// ...

getAll(): Observable<User[]> {
  // Faz de conta que tá vindo do HttpClient 😉
  return of([
    {
      id: "c2fa20d9-e248-4e96-b003-904fc13a3e9f",
      name: "João",
      active: true,
    },
    {
      id: "3ae4e6bf-9622-400e-bda7-5ce110519382",
      name: "Maria",
      active: false,
    },
  ] as any).pipe(map((e) => userSchema.array().parse(e)));
}
Interessante esse uso, nunca pensei em fazer validação de dados vindo da API com Zod. Eu sempre fiz na mão, porque é um procedimento simples. Mas se a gente escalar o payload pra objetos mais complexos, o Zod encaixa certinho.

Gostei muito do post, obrigado por tazer. Estou começando a usar Angular em meus projetos pessoais. Adoraria ver mais sobre ele por aqui.

Obrigado pelo feedback. Vai acompanhando o tabnews, estou o tempo inteiro procurando topicos para fazer novos posts.