Bootcamp
Search…
4.1.6: Sequelize Constraints and Validations

Introduction

Sequelize allows us to define rules for what data we can store in our DB with database constraints and model validations. The DB checks DB constraints at the DB level, i.e. on the DB server, and our app checks model validations at the app level, i.e. on our app servers. Generally we will want both DB constraints and model validations so our system is most robust.

Setup Packages and Folders, Configure DB

Set up Sequelize with a new Node application and configure the DB in the same way we did in Module 4.1.1: Intro to Sequelize. Update config.js to use a new DB name, validations_development to distinguish the DB from other modules. Stop after creating the DB and follow the steps below to create a one-to-many SQL relationship with through table attributes with Sequelize.
Note that it's not strictly necessary to setup a completely new database for this, and you can reuse the database, migrations and seed from 4.1.4, or actually any of the other Sequelize sections if you want.

Migrations: Create Categories and Items Tables

npx sequelize migration:generate --name create-categories-items-tables
Replace the contents of the generated file with the following table-creation code.

Sample Migration File (<GENERATED_DATE>-create-categories-items-tables.js)

allowNull in a migration is an example of a DB constraint. We will see more examples of DB constraints in the Database Constraints section below.
module.exports = {
up: async (queryInterface, Sequelize) => {
// Categories needs to be created first because Items references Categories
await queryInterface.createTable('categories', {
id: {
// allowNull is an example of a DB constraint
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});
​
await queryInterface.createTable('items', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
allowNull: false,
type: Sequelize.STRING,
},
category_id: {
type: Sequelize.INTEGER,
references: {
model: 'categories',
key: 'id',
},
},
created_at: {
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
​
down: async (queryInterface, Sequelize) => {
// items table needs to be dropped first because items references categories
await queryInterface.dropTable('items');
await queryInterface.dropTable('categories');
},
};
Create the categories and items tables in our DB by running migrations.
npx sequelize db:migrate
Verify table creation in psql.
psql -d validations_development

Models: Create Category and Item Models

allowNull in a model file is an example of a model validation. We will see more examples of model validations in the Model Validations section below.

models/category.mjs

export default function initCategoryModel(sequelize, DataTypes) {
return sequelize.define(
'category',
{
id: {
// allowNull is an example of a model validation.
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
name: {
type: DataTypes.STRING,
},
createdAt: {
allowNull: false,
type: DataTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE,
},
},
{
// The underscored option makes Sequelize reference snake_case names in the DB.
underscored: true,
}
);
}

models/item.mjs

export default function initItemModel(sequelize, DataTypes) {
return sequelize.define(
'item',
{
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
name: {
type: DataTypes.STRING,
},
categoryId: {
type: DataTypes.INTEGER,
references: {
model: 'categories',
key: 'id',
},
},
createdAt: {
allowNull: false,
type: DataTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE,
},
},
{
// The underscored option makes Sequelize reference snake_case names in the DB.
underscored: true,
}
);
}

models/index.mjs

import sequelizePackage from 'sequelize';
import allConfig from '../config/config.js';
​
import initItemModel from './item.mjs';
import initCategoryModel from './category.mjs';
​
const { Sequelize } = sequelizePackage;
const env = process.env.NODE_ENV || 'development';
const config = allConfig[env];
const db = {};
​
let sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
​
db.Item = initItemModel(sequelize, Sequelize.DataTypes);
db.Category = initCategoryModel(sequelize, Sequelize.DataTypes);
​
db.Item.belongsTo(db.Category);
db.Category.hasMany(db.Item);
​
db.sequelize = sequelize;
db.Sequelize = Sequelize;
​
export default db;

Seed: Create Seed Data for DB

We do not specify constraints or validations in seeder files. We use the following seeder to seed data to test our constraints and validations in this module.
Use sequelize-cli to generate a seed data migration file.
npx sequelize seed:generate --name seed-data

Sample Seed Data Migration File (<GENERATED_DATE>-seed-data.js)

The following seed data migration file creates 3 categories, and 3 items that reference each of those categories, i.e. 9 items total. All our added to the DB when the seed migration runs.
module.exports = {
up: async (queryInterface) => {
const categoriesList = [
{
name: 'fish',
created_at: new Date(),
updated_at: new Date(),
},
{
name: 'fruit',
created_at: new Date(),
updated_at: new Date(),
},
{
name: 'meat',
created_at: new Date(),
updated_at: new Date(),
},
];
​
// Insert categories before items because items reference categories
let categories = await queryInterface.bulkInsert(
'categories',
categoriesList,
{ returning: true }
);
​
const items = [];
for (let i = 0; i < categories.length; i++) {
const category = categories[i];
​
items.push({
name: 'some item',
category_id: category.id,
created_at: new Date(),
updated_at: new Date(),
});
​
items.push({
name: 'other item',
category_id: category.id,
created_at: new Date(),
updated_at: new Date(),
});
​
items.push({
name: 'iitemmm',
category_id: category.id,
created_at: new Date(),
updated_at: new Date(),
});
}
​
await queryInterface.bulkInsert('items', items);
},
​
down: async (queryInterface) => {
// Delete item before category records because items reference categories
await queryInterface.bulkDelete('items', null, {});
await queryInterface.bulkDelete('categories', null, {});
},
};
Run seed migrations.
npx sequelize db:seed:all

Database Constraints

We set database constraints in migrations, because migrations determine the DB schema. Sequelize will let us know when we violate one of these constraints.

Sample DB Constraint: Allow Null

In the migration we ran above, we told our DB to not allow null values for the name column of the items table. Let's try to violate this constraint.

<GENERATED_DATE>-create-categories-items-table.js

await queryInterface.createTable('items', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
// In our migration above, we set allowNull to false for the name column.
allowNull: false,
type: Sequelize.STRING,
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
},
});

Error Handling

violateDbConstraint.mjs

Create a violateDbConstraint.mjs script to try to violate our DB constraint.
import db from './models/index.mjs';
​
const violateDbConstraint = async () => {
try {
const category = await db.Category.findOne({
where: {
name: [process.argv[2]],
},
});
const associatedItem = await db.Item.create({
name: process.argv[3],
categoryId: category.id,
});
console.log(associatedItem);
} catch (error) {
console.error(error);
}
};
​
violateDbConstraint();

Sample Command

Only specify 1 argument to violateDbConstraint.mjs so process.argv[3] will be undefined.
node violateDbConstraint.mjs fish

Sample Output

When we attempt to violate the constraint we get the following error with debugging output below it.
DatabaseError [SequelizeDatabaseError]: null value in column "name" of relation "items" violates not-null constraint
If we want to catch this DB constraint error in our program and write error-handling logic for the DB constraint error separate from other error-handling logic, we need to import the DatabaseError class from Sequelize. Update violateDbConstraint.mjs to perform different logic for database errors.

violateDbConstraint.mjs

import sequelizePackage from 'sequelize';
import db from './models/index.mjs';
​
const { DatabaseError } = sequelizePackage;
​
const violateDbConstraint = async () => {
try {
const category = await db.Category.findOne({
where: {
name: [process.argv[2]],
},
});
const associatedItem = await db.Item.create({
name: process.argv[3],
categoryId: category.id,
});
console.log(associatedItem);
} catch (error) {
if (error instanceof DatabaseError) {
console.error('This is a database error!');
console.error(error);
} else {
console.error(error);
}
}
};
​
violateDbConstraint();
We would generally not use DatabaseError to catch a null constraint like we did in the first example, because SQL also throws DatabaseErrors when SQL queries fail, for example when we misspell the name of a table in the model file. DB errors also do not provide the English-formatted error text as in the validation error example below. Always add validation to models whenever possible, for example in the case of null-checking.
More info on the differences between typeof and instanceof operators here.

Other Constraints

We can write similar DB constraints and logic for properties such as uniqueness and foreign key validity. Read more on Sequelize DB constraints here: https://sequelize.org/master/manual/validations-and-constraints.html.

Model Validations

We can specify constraints on our models as well, such that we can detect constraint violations before making DB queries. This is important because network requests such as DB queries take time, and if we can detect violations without making network requests we can make our apps more responsive. Constraints are called "validations" in the model context.

Sample Model Validation: Allow Null

For example, if we wanted to validate the presence of a field before running a DB query, we could add a non-null validation to the model.

models/item.mjs

Update our item.mjs model to add an allowNull validation for the name attribute.
export default function itemModel(sequelize, DataTypes) {
return sequelize.define(
'item',
{
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
name: {
// This validation is new.
allowNull: false,
type: DataTypes.STRING,
},
createdAt: {
allowNull: false,
type: DataTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE,
},
},
{
// The underscored option makes Sequelize reference snake_case names in the DB.
underscored: true,
}
);
}

Error Handling

Let's violate the model violation we just added on the name attribute. Let's create a violateModelValidation.mjs script that performs special logic for validation errors by importing the ValidationError class from Sequelize.

violateModelValidation.mjs

import sequelizePackage from 'sequelize';
import db from './models/index.mjs';
​
const { ValidationError } = sequelizePackage;
​
const violateModelValidation = async () => {
try {
const category = await db.Category.findOne({
where: {
name: [process.argv[2]],
},
});
const associatedItem = await db.Item.create({
name: process.argv[3],
categoryId: category.id,
});
console.log(associatedItem);
} catch (error) {
if (error instanceof ValidationError) {
console.error('This is a validation error!');
console.error(error);
} else {
console.error(error);
}
}
};
​
violateModelValidation();

Other Validations

Let's add more validations to our model and violate them. There is a wider range of model validations available than DB constraints. For a full list of built-in validations, see Sequelize Model Validation docs here.

models/item.mjs

Notice the validations for the name attribute.
export default function itemModel(sequelize, DataTypes) {
return sequelize.define(
'item',
{
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER,
},
name: {
// The following custom validations are new.
validate: {
// isAlpha allows only alphanumeric characters.
isAlpha: true,
// This only allows strings of length 3 to 23.
len: [3, 23],
},
type: DataTypes.STRING,
},
categoryId: {
type: DataTypes.INTEGER,
references: {
model: 'categories',
key: 'id',
},
},
createdAt: {
allowNull: false,
type: DataTypes.DATE,
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE,
},
},
{
// The underscored option makes Sequelize reference snake_case names in the DB.
underscored: true,
}
);
}

Validation Error Objects

Validation errors are structured so we can handle such errors gracefully. We could show the user what happened by parsing the data in the ValidationErrorItem object and sending an error message to the client via a response. This message could be displayed in the form with invalid input client-side.

Sample Errors Object

errors: [
ValidationErrorItem {
message: 'Validation len on name failed',
type: 'Validation error',
path: 'name',
value: 'h',
origin: 'FUNCTION',
instance: [Item],
validatorKey: 'len',
validatorName: 'len',
validatorArgs: [Array],
original: [Error]
}
]

Combining Constraints and Validations

violateModelValidation.mjs

Update violateModelValidation.mjs to include the following error-handling logic.
import sequelizePackage from 'sequelize';
import db from './models/index.mjs';
​
const { ValidationError, DatabaseError } = sequelizePackage;
​
db.Category.findOne({
where: {
name: [process.argv[2]],
},
})
.then((category) => {
return db.Item.create({
name: process.argv[3],
categoryId: category.id,
});
})
.then((item) => {
console.log(item);
})
.catch((error) => {
if (error instanceof ValidationError) {
console.error('This is a validation error!');
console.error(error);
console.error('The following is the first error message:');
console.error(error.errors[0].message);
} else if (error instanceof DatabaseError) {
console.error('This is a database error!');
console.error(error);
} else {
console.error(error);
}
});

Sample Commands

Try the following sample commands to observe what error messages show when we violate the constraints and validations in different ways.
node violateModelValidation.mjs fish some-item
node violateModelValidation.mjs fish
node violateModelValidation.mjs fish a
node violateModelValidation.mjs fish ababababababababababababababababababababababababababa

Further Reading

  1. 1.
  2. 2.
    See a full list of possible Sequelize errors here: https://sequelize.readthedocs.io/en/latest/api/errors/​