Validate like a pro everywhere with yup
Tags: react, formik, yup, typescript
Published: 2021-05-01 21:29:48

In this post I will show you my approach on scalable user input validation. [Yup](https://github.com/jquense/yup) is the essential library to help me achieve this goal. I also use [express](https://github.com/expressjs/express), [react](https://github.com/facebook/react/) and [formik](https://github.com/formium/formik).
### One function to validate - one to handle them all

The main helper funcions are `validateInput` and `handleFieldErrors`. You may define them their own package because `validateInput` is useful for client and server side projects. 

It receives a yup-Schema and any input and will return the input if it was valid or throw a `ValidationError` if there is any:

```typescript
export const validateInput = async <T>(
  schema: ObjectSchema<any>,
  input: any
): Promise<T> => {
  await schema.validate(input, { abortEarly: false });
  return schema.cast(input);
};
```

The function is quite simple, the only important detail here, is the `schema.cast(input)` and the generic return type that will help to get the right typescript type for better auto-completion. More information on this magic can be found in the [yup documentation](https://github.com/jquense/yup/blob/master/docs/typescript.md).

### Client-side Usage
To use it you just have to define you schema and `await` it:

```typescript
const schema = object({ name: string().required() }) 
const validatedInput = await validateInput<Asserts<typeof schema>>(
  schema,
  notValidatedInupt
);
```

Note that we feed the generic with `Asserts<>`, which is exported by `yup`.

In formiks `onSubmit` callback you can catch the error from `validateInput` and map them to the fields:

```typescript
// onSubmit={async (values, { setFieldError }) => {
try {
  const schema = object({
    name: string().required(),
    age: number()
      .transform((value, original) =>
        original == null || original === "" ? undefined : value
      )
      .required(),
  });

  const validatedInput = await validateInput<Asserts<typeof schema>>(
    schema,
    values
  );

  setResult(`${validatedInput.name} is now ${validatedInput.age}`);
} catch (error) {
  if (error instanceof ValidationError) {
    error.inner.forEach(({ path, message }) => {
      if (path != null) {
        setFieldError(path, message);
      }
    });
  }
}
```

Of course you can outsource the `catch` part, but do not forget to catch other errors!

```typescript
export const handleFieldErrors = (
  error: any,
  setFieldError: (fieldKey: string, errorMessage: string) => void
) => {
  if (error instanceof ValidationError) {
    error.inner.forEach(({ path, message }) => {
      if (path != null) {
        setFieldError(path, message);
      }
    });
  } else {
    throw error;
  }
};
```
### Server side usagae

Its is basically the same, but there is one caveat:

```typescript
app.post("/", async (req, res) => {
  try {
    const bodySchema = object({
      name: string().required().notOneOf(["admin"]),
      age: number()
        .transform((value, original) =>
          original == null || original === "" ? undefined : value
        )
        .required(),
    });

    const { age, name } = await validateInput<Asserts<typeof bodySchema>>(
      bodySchema,
      req.body
    );

    return res.json({ age, name });
  } catch (error) {
    res.status(400);
    res.json(error);
  }
});
```

The `instanceof` will no longer work since the backend will just return plain JSON to our client. So if your want to use the errors from your node backend, you either have to catch them, map them to a `ValidationError` and throw them to `handleFieldErrors` or give some trust to Typescript and yup like so:

```typescript
if (error instanceof ValidationError || error.inner != null) {
  //...
}
```

You can also use this pattern to validate `req.params` or `req.query`. Because it will return the valid and typescript safe input, you will not have a hard time finding the properties with your auto-completion. 

### Combined Powers

As a result you can have both client and server side validation or just server side validation, without changing the catch handler.

{% details App.js handling backend and frontend validation errors  %} 
```typescript

const submitLocal = async (values: any) => {
  await new Promise((resolve) => setTimeout(resolve, 100));
  const schema = object({
    name: string().required(),
    age: number()
      .transform((value, original) =>
        original == null || original === "" ? undefined : value
      )
      .required(),
  });

  const validatedInput = await validateInput<Asserts<typeof schema>>(
    schema,
    values
  );

  return `${validatedInput.name} is now ${validatedInput.age}`;
};

const submitBackend = async (values: any) => {
  const response = await fetch(`/`, {
    method: "POST",
    body: JSON.stringify(values),
    headers: {
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw error;
  }

  const { age, name } = await response.json();
  return `${name} is now ${age}`;
};

export default function App() {
  const [result, setResult] = useState<string | void>();
  return (
    <div className="App">
      <Formik
        initialValues={{ age: "", name: "" }}
        onSubmit={async (values, { setFieldError }) => {
          setResult();
          try {
            await submitLocal(values);
            const nextResult = await submitBackend(values);
            setResult(nextResult);
          } catch (error) {
            handleFieldErrors(error, setFieldError);
          }
        }}
      >
// fields and friends ;) 

```
{% enddetails %}

### Notes

#### the number transform hack

```typescript
.transform((value, original) =>
  original == null || original === "" ? undefined : value
)
```

Since `required` will only [grumble on](
https://github.com/jquense/yup#mixedrequiredmessage-string--function-schema) `null`, `undefined` or (if it is a `string()`) `''`, but `number()` will cast to a valid number or `NaN`. So you might want to check the original Value to prevent `NaN` in your validated input ([more information](https://github.com/jquense/yup/issues/634)). 

### The End

Thanks for reading this post. If you want you can leave some feedback down below since this is my first post I would appreciate it :pray:.