Unified Validation & Typings in Web-Apps using yup
Tags: react, nestjs, yup, webdev
Published: 2021-10-27 07:08:46

## The root Issue
In most cases defining and implementing Types is a repetitive (and nasty) Task for Full-Stack developers. This usually includes implementing kind of the same stuff in multiple locations:
* Entity Types in the DB-Layer
* Validation Schemas for Request Data
* Response Types for the API-Layer (GraphQL or REST)
* (Prop-) Types and Validation for Forms in the Frontend

## How to tackle this issue?
One way I figured when using NestJS in combination with React is to use `yup` (in combination with other third party libraries though). 
In React we can utilize Formik which natively supports validation via `yup` schemas and in the NestJS Backend we can use `nestjs-yup` which is quite handy and straight forward to use as well. Btw: This works for both, GraphQL- as well as Rest-APIs built with Nest. 👌

### Step 1) Shared library: Schema implementation & Type definition
So let’s start off with a central place (a shared library for instance) where we’ll define the schemas as well as the actual types.


`IPerson.ts`
```typescript
export const PersonSchema = yup.object({
  firstName: yup
    .string()
    .min(2, "Too Short!")
    .max(50, "Too Long!")
    .required("Required"),
  lastName: yup
    .string()
    .min(2, "Too Short!")
    .max(50, "Too Long!")
    .required("Required"),
  email: yup.string().email("Invalid email").required("Required"),
});

export const UpdatePersonSchema = BaseSchema.concat(
  yup.object({
    firstName: yup.string().notRequired(),
    lastName: yup.string().notRequired(),
    email: yup.string().email("Invalid email").notRequired(),
  })
);

export interface IPerson {
  firstName: string;
  lastName: string;
  email: string;
}

export interface IUpdatePerson extends IUpdateBase, Partial<IPerson> {}
```

Another way to let `yup` generate the types automatically is the following:
```typescript
type PersonType = yup.InferType<typeof PersonSchema>;
``` 
In the long term I found this less useful since there’s a lot of internal Typings that prevent straight forward error messages. Furthermore optionals `?` won’t work at all when implementing the interfaces in e.g. entities.


### Step 2) Backend: Entity / Response Type definition 
Here we’ll make use of the library `nestjs-yup` which will provide the necessary Decorators for easy usage.

First step here is to implement the Entity (the ORM Framework used in this example is `typeorm`). The important part here is that we can use the interfaces defined in the shared type so our Entity is forced to implement the fields defined in `IPerson` (hence requiring adjustments once something changed in the interface declaration). 


`person.entity.ts` 
```typescript
@Entity()
@ObjectType()
export class Person extends Base implements IPerson {
  @Field()
  @Column("text")
  firstName: string;

  @Field()
  @Column("text")
  lastName: string;

  @Field()
  @Column("text")
  email: string;
}
```

When creating a new User we’ll use the validation logic implemented in the UserSchema (requiring a `password` as well as a `username`). The Decorator `@UseSchema(Schema)` will register the Schema internally to be used by the `YupValidationPipe` later on automatically.


`create-person.input.ts`
```typescript
@InputType()
@UseSchema(PersonSchema)
export class CreatePersonInput implements IPerson {
  @Field()
  firstName: string;

  @Field()
  lastName: string;

  @Field()
  email: string;
}
```

For the Person-Update-Type we’ll make use of Partial Types which will basically mark all attributes as optional (which we did in the Schema as well). So we have to declare the Fields as `nullable` and register the `UseSchema` for this Input-Type.


`update-person.input.ts`
```typescript
@InputType()
export class UpdatePersonInput
  extends PartialType(CreatePersonInput)
  implements IUpdatePerson
{
  @Field(() => ID)
  id: string;
}
```

Last but not least we will register the `YupValidationPipe` globally so each and every Endpoints using any of the Classes decorated with `@UseSchema(Entity)` will be validated automatically using the schema that was given to the decorator.


`main.ts` 
```typescript
// … 
const app = await NestFactory.create(AppModule);
…
app.useGlobalPipes(new YupValidationPipe());
…
```
Another option would be to just decorate each and every desired Endpoint with
```typescript
@UsePipes(new YupValidationPipe())
```
to validate the request data.

### Frontend: Form Types / Props definition

In our React App we’ll create a plain and simple Form-Component to validate the data entered to supposedly create a new Person (without any actual update or creation calls to the backend).


`person.tsx`
```typescript
const initialPerson = {
  firstName: "",
  lastName: "",
  email: "",
} as IPerson;

export const Person = () => (
  <div>
    <h1>Person</h1>
    <Formik
      initialValues={initialPerson}
      validationSchema={PersonSchema}
      onSubmit={(values) => {
        console.log("submitting: ", { values });
      }}
    >
      {({ errors, touched }) => (
        <Form>
          <div className={`${styles.flex} ${styles.column}`}>
            <Field name="firstName" placeholder="FirstName" />
            {errors.firstName && touched.firstName ? (
              <div>{errors.firstName}</div>
            ) : null}
            <Field name="lastName" placeholder="LastName" />
            {errors.lastName && touched.lastName ? (
              <div>{errors.lastName}</div>
            ) : null}
            <Field name="email" placeholder="E-Mail" />
            {errors.email && touched.email ? <div>{errors.email}</div> : null}
            <button type="submit">Submit</button>
          </div>
        </Form>
      )}
    </Formik>
  </div>
);
```

And that's it 🙌 Well at least for now, handling the creation of a new Person and updating an existing Person will follow (probably in my next Post). 😊

## Conclusion
To be fair: it's not the "one-size-fits-all" kind of solution since validation for the DB-Layer (via `@Column({nullable: true})`) still has to be added manually. BUT it makes dealing with the same types in the Frontend as well as the Backend much easier because all of them are based on the same shared interface. So if something changes there ts-compiler will complain when e.g. running the tests and you'll know which places will have to be adjusted accordingly.  
Another practice or habit I found is that you can use the convention to set e.g. the `Field` as well as the `Column` to `nullable: true` once the attribute of the implemented interface is optional `?`. 

You can find the code [here on Github](https://github.com/kevelopment/yup-unified-validation). 🥳