Adding Automatic Audit Fields with Sequelize Model Hooks
Learn how to automatically populate audit fields like createdBy and updatedAt using Sequelize model hooks, with transaction safety and verification steps.
10 Jul 2025, 14:10 UTC

Desired outcome
Every time a model instance is created or updated, Sequelize automatically populates createdAt, updatedAt, createdBy and updatedBy fields without scattering this logic across service files.
Prerequisites
- Node.js >= 14 installed
- A Sequelize project (v5 or v6) with a configured database connection
- Basic knowledge of defining Sequelize models
- Permission to run Node scripts and execute migrations/seeders if needed
Procedure
- Define the model with audit columns
const { DataTypes, Model } = require('sequelize'); const sequelize = require('./sequelize-instance'); // your configured sequelize class AuditModel extends Model {} AuditModel.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.STRING, allowNull: false }, // audit fields createdAt: { type: DataTypes.DATE, allowNull: false }, updatedAt: { type: DataTypes.DATE, allowNull: false }, createdBy: { type: DataTypes.STRING, allowNull: true }, updatedBy: { type: DataTypes.STRING, allowNull: true } }, { sequelize, modelName: 'AuditModel', timestamps: false // we manage timestamps ourselves via hooks }); - Add
beforeCreateandbeforeUpdatehooksAuditModel.addHook('beforeCreate', (instance, options) => { const now = new Date(); instance.createdAt = now; instance.updatedAt = now; // set creator from options or fallback instance.createdBy = options.auditUser ?? 'system'; instance.updatedBy = options.auditUser ?? 'system'; }); AuditModel.addHook('beforeUpdate', (instance, options) => { instance.updatedAt = new Date(); instance.updatedBy = options.auditUser ?? 'system'; }); - Use the hooks in a transaction (optional but recommended)
async function createRecord(name, user) { const t = await sequelize.transaction(); try { const record = await AuditModel.create( { name }, { transaction: t, auditUser: user } // passes user to hooks ); await t.commit(); return record; } catch (err) { await t.rollback(); throw err; } } async function updateRecord(id, newName, user) { const t = await sequelize.transaction(); try { const [affected] = await AuditModel.update( { name: newName }, { where: { id }, transaction: t, auditUser: user, individualHooks: true } ); await t.commit(); return affected; } catch (err) { await t.rollback(); throw err; } } - Verify the hook execution
- Run a script that calls
createRecord('test-item', 'alice'). - Inspect the returned instance: it should have
createdAt,updatedAtset to near‑current timestamps andcreatedBy=updatedBy= 'alice'. - Call
updateRecord(id, 'renamed', 'bob')and verify thatupdatedAtchanges andupdatedBybecomes 'bob' whilecreatedBystays 'alice'. - To test rollback, make the
beforeUpdatehook throw an error (e.g.,if (newName === 'fail') throw new Error('blocked')) and callupdateRecord. After catching the error, query the row; the name must remain unchanged, proving the transaction rolled back.
- Run a script that calls
Expected checks
- Console logs (if you add them) show hook entry in registration order.
- Returned model instances contain the audit fields with expected values.
- When a hook throws, the transaction is rolled back and the database row is unchanged.
Recovery options (rollback)
If a hook throws an error, Sequelize automatically aborts the current transaction and rolls back all changes made in that transaction, including any changes performed by earlier hooks. No manual cleanup is required; just ensure the operation is wrapped in a transaction (as shown) or relies on Sequelize’s implicit transaction when transaction option is provided.
Limitations
- Hooks do not run for raw SQL (
sequelize.query) or forModel.update/Model.destroyunlessindividualHooks: trueis set. - Avoid long‑running synchronous work inside hooks; it blocks the Node event loop. Defer heavy tasks to background queues.
- Updating the same model inside an
afterUpdateorafterCreatehook can cause infinite loops. Guard against re‑entry by checking a custom flag (e.g.,instance._auditHookSkipped) or by limiting hook logic to specific attribute changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.