Node Migrate
Node Migrate is an abstract migration framework designed for Node.js applications, offering both a command-line interface (CLI) and a programmatic API for managing schema or data changes. As of its latest version, 2.1.0, it provides a flexible mechanism to create, apply, and rollback migrations, supporting various database types through custom logic within migration files. It distinguishes itself by providing a simple, generator-based approach to migration file creation, allowing users to define their own templates and compilers (like Babel or TypeScript) for modern JavaScript features. The package prioritizes flexibility, enabling developers to integrate it into diverse project setups. Its release cadence is not explicitly stated but appears to be stable with infrequent major updates, making it a reliable choice for established Node.js projects requiring robust migration management.
Common errors
-
TypeError: next is not a function
cause An asynchronous (async) migration function attempted to call the `next()` callback, which is only expected by callback-based migration functions.fixRemove the `next()` call from any migration function declared with the `async` keyword. -
Error: Migration failed: ... - [Error: ENOENT: no such file or directory, open '.migrate']
cause The `stateStore` file specified in the configuration (default `.migrate`) does not exist, and `migrate` was unable to create it due to insufficient permissions or an invalid path.fixVerify that the directory containing the `stateStore` path is writable by the process, or manually create an empty `.migrate` file before running migrations. -
Error: Could not find migration: <name>
cause The migration specified by `<name>` was not found in the `migrationsDirectory`, or the `migrationsDirectory` option is pointing to an incorrect location.fixDouble-check the `migrationsDirectory` configuration. Ensure the migration file's name (specifically the title part, e.g., 'add-users') matches what you're trying to `up` or `down` to. -
SyntaxError: Cannot use import statement outside a module
cause Attempting to use an ES module `import` statement to load the `migrate` package in a Node.js project that is configured for CommonJS modules, or when `migrate` itself is a CJS module.fixReplace `import migrate from 'migrate';` with `const migrate = require('migrate');`.
Warnings
- gotcha Migrations using `async` functions should *not* call the `next()` callback. Calling both will result in a `TypeError` (e.g., 'next is not a function') or other unexpected behavior, as `next()` is intended for traditional callback-based asynchronous operations.
- gotcha The `stateStore` option is critical for tracking migration history. Using an ephemeral state store (e.g., `/dev/null`), or one located in a non-persistent or unwritable directory, can lead to migrations running multiple times or being out of sync on subsequent runs.
- gotcha Migration files are ordered strictly by their timestamp prefix. Manually renaming files or creating them without using the `migrate create` CLI command can lead to an incorrect migration order or `ENOENT` errors if files are not found.
- gotcha The `migrate` package is distributed as a CommonJS module. Attempting to use ES module `import` syntax (e.g., `import migrate from 'migrate'`) directly in a pure ESM Node.js environment without a bundler, custom loader, or transpilation will result in a `SyntaxError`.
- gotcha When using custom compilers (e.g., Babel, TypeScript) for your migration files, the `--compiler` flag must be consistently applied to both the `migrate create` command (if the generator needs it) and the `migrate up`/`migrate down` commands. Inconsistent application will lead to execution failures.
Install
-
npm install migrate -
yarn add migrate -
pnpm add migrate
Imports
- migrate
import migrate from 'migrate';
const migrate = require('migrate'); - migrate.load
migrate.load({ stateStore: '.migrate' }, function (err, set) { /* ... */ }); - Migration `up` and `down` functions
module.up = function () { ... }exports.up = function (next) { /* ... */ next(); } exports.down = async function () { /* ... */ }
Quickstart
import migrate from 'migrate';
import { resolve } from 'path';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
// Mock database object shared across migrations.
// In a real application, this would be your actual database client or ORM connection.
const mockGlobalDb: { users?: string[]; products?: string[]; } = {};
// Paths for migrations and state store
const migrationsDir = resolve('./migrations_quickstart_simple');
const stateStorePath = resolve('./.migrate_quickstart_simple_state');
// Ensure directories and state file exist
if (!existsSync(migrationsDir)) mkdirSync(migrationsDir);
if (!existsSync(stateStorePath)) {
writeFileSync(stateStorePath, JSON.stringify({ lastRun: null, migrations: [] }), 'utf8');
}
// Create mock migration files
const createUsersMigrationContent = `
// This 'db' variable refers to 'mockGlobalDb' from the quickstart example.
// In real migrations, you would typically import or pass your database client.
exports.up = function (next) {
console.log('Running UP: Create Users');
// Simulate a database operation
module.parent.exports.mockGlobalDb.users = ['Alice', 'Bob'];
next();
};
exports.down = function (next) {
console.log('Running DOWN: Drop Users');
// Simulate rolling back a database operation
delete module.parent.exports.mockGlobalDb.users;
next();
};
`;
const addProductsMigrationContent = `
// This 'db' variable refers to 'mockGlobalDb' from the quickstart example.
exports.up = async function () { // Using async/await, no 'next' callback needed
console.log('Running UP: Add Products');
// Simulate a database operation
module.parent.exports.mockGlobalDb.products = ['Laptop', 'Mouse'];
};
exports.down = async function () {
console.log('Running DOWN: Remove Products');
// Simulate rolling back a database operation
delete module.parent.exports.mockGlobalDb.products;
};
`;
// Write the migration files for the quickstart to execute
const ts1 = Date.now() - 10000;
const ts2 = Date.now();
writeFileSync(resolve(migrationsDir, `${ts1}-create-users.js`), createUsersMigrationContent, 'utf8');
writeFileSync(resolve(migrationsDir, `${ts2}-add-products.js`), addProductsMigrationContent, 'utf8');
// Expose mockGlobalDb so migration files (which are 'require'd) can access it.
// This is a quickstart hack; in a real app, you'd pass your DB client correctly.
Object.assign(module.exports, { mockGlobalDb });
console.log('Initializing migrations...');
migrate.load({
stateStore: stateStorePath,
migrationsDirectory: migrationsDir
}, function (err, set) {
if (err) {
console.error('Failed to load migrations:', err);
process.exit(1);
}
console.log('Migrations loaded. Current status:');
set.migrations.forEach(m => console.log(`- ${m.title}: ${m.state}`));
set.up(function (err) {
if (err) {
console.error('Failed to run UP migrations:', err);
process.exit(1);
}
console.log('\nAll UP migrations successfully executed!');
console.log('Current mock database state:', mockGlobalDb);
// To demonstrate rolling back, uncomment the following:
/*
console.log('\nRunning DOWN to "create-users" migration...');
set.down('create-users', function(err) {
if (err) {
console.error('Failed to run DOWN migrations:', err);
process.exit(1);
}
console.log('Successfully rolled back to "create-users"!');
console.log('Current mock database state after partial DOWN:', mockGlobalDb);
});
*/
});
});