← Back to DevBytes

Yup from Beginner to Expert: A Learning Path

Yup from Beginner to Expert: A Learning Path

Form validation is one of those tasks every developer encounters, and doing it well can be the difference between a smooth user experience and a frustrating one. Yup is a JavaScript schema builder for value parsing and validation. It allows you to define schemas that describe the shape and constraints of your data, then validate values against those schemas in a clean, declarative way. In this tutorial, we will walk through Yup from the basics all the way to advanced patterns used by seasoned developers.

What is Yup?

Yup is a lightweight, schema-based validation library inspired by Joi but designed to work seamlessly in the browser and Node.js. A "schema" in Yup is an object that describes the expected structure, types, and constraints of your data. You can validate objects, strings, numbers, arrays, dates, and even custom types. Yup also supports type casting, which means it can transform input values into the types your schema expects.

Why Yup Matters

Writing validation logic by hand quickly becomes error-prone and hard to maintain. You end up with long chains of if statements, duplicated logic, and inconsistent error messages. Yup solves this by letting you express validation rules declaratively. This matters because:

Getting Started: Installation and First Schema

Install Yup using your preferred package manager:

npm install yup
# or
yarn add yup
# or
pnpm add yup

Let's create a simple schema that validates a user object:

import * as yup from 'yup';

const userSchema = yup.object({
  name: yup.string().required('Name is required'),
  email: yup.string().email('Must be a valid email').required('Email is required'),
  age: yup.number().positive().integer().required(),
});

const validUser = {
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
};

userSchema.validate(validUser)
  .then(value => console.log('Valid:', value))
  .catch(err => console.log('Error:', err.errors));

Here we defined an object schema with three fields. Each field has its own type and constraints. The validate method returns a promise that resolves with the validated (and optionally cast) value or rejects with an error containing the errors array of messages.

Core Concepts: Types and Methods

Yup provides schema constructors for common types: string, number, boolean, date, array, object, mixed (for any value), and tuple schemas in newer versions. Each schema type exposes a fluent API of validation methods.

Common string methods include min, max, matches, email, url, required, and oneOf. Number methods include min, max, positive, negative, integer, and lessThan. Here is a richer example:

const passwordSchema = yup.string()
  .min(8, 'Password must be at least 8 characters')
  .matches(/[A-Z]/, 'Must contain an uppercase letter')
  .matches(/[0-9]/, 'Must contain a number')
  .matches(/[^A-Za-z0-9]/, 'Must contain a special character')
  .required('Password is required');

Validating Objects and Nested Structures

Real-world data is rarely flat. Yup handles nested objects and arrays naturally:

const orderSchema = yup.object({
  orderId: yup.string().uuid().required(),
  customer: yup.object({
    name: yup.string().required(),
    email: yup.string().email().required(),
  }).required(),
  items: yup.array(
    yup.object({
      sku: yup.string().required(),
      quantity: yup.number().integer().positive().required(),
      price: yup.number().positive().required(),
    })
  ).min(1, 'Order must contain at least one item'),
  shippingAddress: yup.object({
    street: yup.string().required(),
    city: yup.string().required(),
    zip: yup.string().matches(/^\d{5}$/, 'Invalid zip code').required(),
  }),
});

Notice how yup.array takes a sub-schema that each element must satisfy. Nested objects are simply yup.object schemas embedded within the parent.

Conditional Validation with when

One of Yup's most powerful features is conditional validation using when. This lets you change the schema for a field based on the value of sibling fields:

const registrationSchema = yup.object({
  accountType: yup.string().oneOf(['personal', 'business']).required(),
  companyName: yup.string().when('accountType', {
    is: 'business',
    then: schema => schema.required('Company name is required for business accounts'),
    otherwise: schema => schema.notRequired(),
  }),
  taxId: yup.string().when('accountType', {
    is: 'business',
    then: schema => schema.required('Tax ID is required for business accounts'),
    otherwise: schema => schema.notRequired(),
  }),
});

The when method accepts a field name (or array of names) and a configuration object. The is predicate determines which branch to use, and then / otherwise return the modified schema.

Custom Validation with test

When built-in methods are not enough, you can write custom validators using test:

const usernameSchema = yup.string().test(
  'unique-username',
  'Username is already taken',
  async function (value) {
    if (!value) return true;
    const isAvailable = await checkUsernameAvailability(value);
    return isAvailable;
  }
);

The test method accepts a name, a message, and a function that returns a boolean or a promise. Inside the test function, this gives you access to the schema context and the createError helper for more control:

const evenNumberSchema = yup.number().test(
  'is-even',
  'Number must be even',
  function (value) {
    if (value === undefined) return true;
    if (value % 2 !== 0) {
      return this.createError({ message: `${value} is not even` });
    }
    return true;
  }
);

Working with Context and Cross-Field Validation

Sometimes you need data that is not part of the validated object. Yup lets you pass context to validate:

const transferSchema = yup.object({
  amount: yup.number().positive().required(),
}).test(
  'sufficient-balance',
  'Insufficient balance',
  function (value) {
    const balance = this.options.context?.balance ?? 0;
    return value && value.amount <= balance;
  }
);

transferSchema.validate(
  { amount: 500 },
  { context: { balance: 300 } }
).catch(err => console.log(err.errors));

This pattern is useful for server-side validation where you need to check against database values without embedding them in the payload.

Validation Options: abortEarly and stripUnknown

By default, Yup stops validating at the first error. For forms, you usually want all errors at once. Pass abortEarly: false:

try {
  await schema.validate(values, { abortEarly: false });
} catch (err) {
  // err.inner is an array of all validation errors
  const formErrors = err.inner.reduce((acc, error) => {
    acc[error.path] = error.message;
    return acc;
  }, {});
}

Other useful options include stripUnknown to remove fields not in the schema, strict to disable type casting, and recursive to control nested validation.

Integrating Yup with React Hook Form

Yup pairs naturally with React Hook Form via the @hookform/resolvers package:

import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required(),
});

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register('password')} />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

Yup and TypeScript: Generating Types

Keeping TypeScript types in sync with validation schemas is a common pain point. The yup package exports a type inference helper. In newer versions, you can use InferType:

import * as yup from 'yup';

const productSchema = yup.object({
  id: yup.string().required(),
  name: yup.string().required(),
  price: yup.number().positive().required(),
  tags: yup.array(yup.string()).default([]),
});

type Product = yup.InferType<typeof productSchema>;
// Equivalent to:
// { id: string; name: string; price: number; tags: string[] }

For more advanced scenarios, libraries like @sinclair/typebox or code generators can produce both Yup schemas and TypeScript types from a single source of truth.

Best Practices

Advanced Pattern: Schema Composition and Reuse

As your application grows, you will want to share schemas. Yup schemas are immutable, so methods return new schema instances. You can extend them with concat or shape:

const baseAddressSchema = yup.object({
  street: yup.string().required(),
  city: yup.string().required(),
  zip: yup.string().required(),
});

const billingAddressSchema = baseAddressSchema.shape({
  zip: yup.string().matches(/^\d{5}$/, 'Invalid zip'),
});

const shippingAddressSchema = baseAddressSchema.concat(
  yup.object({
    deliveryInstructions: yup.string().max(200),
  })
);

This approach lets you build a library of small schemas and assemble them into complex shapes without repeating yourself.

Conclusion

Yup takes the pain out of validation by turning it into a declarative, composable, and reusable exercise. Starting from simple string and number checks, you can grow into nested objects, conditional rules, custom async tests, and full integration with your form and API layers. By following best practices like modular schemas, descriptive messages, and server-side reuse, you will build validation logic that is robust, maintainable, and pleasant to work with. Whether you are validating a login form or a complex multi-step checkout flow, Yup gives you the tools to handle it with confidence.

— Ad —

Google AdSense will appear here after approval

← Back to all articles