Seeders
Seeders allow you to populate your database with initial or test data in a structured, repeatable way.
Overview
Seeders are useful for:
- Development: Populate your local database with test data
- Testing: Create consistent test fixtures
- Production: Insert initial/default data (admin users, configuration, etc.)
Creating a Seeder
Use the CLI to generate a seeder file:
hysteria create:seeder user_seeder
This creates a timestamped file in your seeders directory:
database/seeders/1234567890_user_seeder.ts
The generated seeder extends BaseSeeder:
import { BaseSeeder } from "hysteria-orm";
export default class extends BaseSeeder {
/**
* Run the seeder
*/
async run(): Promise<void> {
console.log("Seeder completed");
}
}
Seeder Configuration
Configure seeder behavior in your SqlDataSource instance:
import { SqlDataSource } from "hysteria-orm";
const sqlDs = new SqlDataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "root",
password: "root",
database: "mydb",
seeders: {
path: "database/seeders", // Seeder files location
tsconfig: "./tsconfig.json", // TypeScript config (optional)
},
});
Writing Seeders
Using Raw Queries
The simplest approach - execute SQL directly:
import { BaseSeeder } from "hysteria-orm";
export default class extends BaseSeeder {
async run(): Promise<void> {
await this.sqlDataSource.rawQuery(`
INSERT INTO users (name, email, password, role) VALUES
('Admin User', 'admin@example.com', 'hashed_password', 'admin'),
('Test User', 'test@example.com', 'hashed_password', 'user')
ON CONFLICT (email) DO NOTHING
`);
console.log("✓ User seeder completed");
}
}
Using Models
Leverage your ORM models for type safety:
import { BaseSeeder } from "hysteria-orm";
import { User } from "../models/user";
export default class extends BaseSeeder {
async run(): Promise<void> {
// Insert single record
await this.sqlDataSource.from(User).insert({
name: "Admin",
email: "admin@example.com",
password: "hashed_password",
role: "admin",
});
// Insert multiple records
await this.sqlDataSource.from(User).insertMany([
{ name: "User 1", email: "user1@example.com", password: "pass" },
{ name: "User 2", email: "user2@example.com", password: "pass" },
]);
console.log("✓ User seeder completed");
}
}
Using Factories
Combine seeders with factories for realistic test data:
import { BaseSeeder } from "hysteria-orm";
import { defineModelFactory } from "hysteria-orm";
import { User } from "../models/user";
import { faker } from "@faker-js/faker";
export default class extends BaseSeeder {
async run(): Promise<void> {
// Create 50 random users
const factory = defineModelFactory(User, {
name: faker.person.fullName(),
email: faker.internet.email(),
password: faker.internet.password(),
age: faker.number.int({ min: 18, max: 80 }),
});
await factory.create(50);
console.log("✓ Created 50 users");
}
}
Environment-Aware Seeding
Run different seeders based on environment:
import { BaseSeeder } from "hysteria-orm";
export default class extends BaseSeeder {
async run(): Promise<void> {
const isProduction = process.env.NODE_ENV === "production";
if (isProduction) {
// Only essential data in production
await this.sqlDataSource.rawQuery(`
INSERT INTO settings (key, value) VALUES
('app_name', 'My App'),
('version', '1.0.0')
ON CONFLICT (key) DO NOTHING
`);
} else {
// Rich test data in development
await this.sqlDataSource.rawQuery(`
INSERT INTO users (name, email, role) VALUES
('Dev User 1', 'dev1@test.com', 'user'),
('Dev User 2', 'dev2@test.com', 'user'),
('Admin', 'admin@test.com', 'admin')
`);
}
console.log("✓ Environment-specific seeder completed");
}
}
Running Seeders
Run All Seeders
Execute all seeders in your configured directory:
hysteria seed -d database/index.ts
Run Specific Files
Run one or more specific seeder files:
# Single file
hysteria seed -s database/seeders/1234_user_seeder.ts -d database/index.ts
# Multiple files (comma-separated)
hysteria seed -s database/seeders/1234_user.ts,database/seeders/5678_post.ts -d database/index.ts
Run All Seeders in a Folder
Specify a folder to run all seeders within it:
hysteria seed -s database/seeders -d database/index.ts
Mix Folders and Files
You can combine folders and specific files:
hysteria seed -s database/seeders/core,database/seeders/special/admin.ts -d database/index.ts
CLI Options
Create Seeder Command
hysteria create:seeder <name> [options]
Options:
-j, --javascript- Generate JavaScript file instead of TypeScript-s, --seeder-path <path>- Custom path for the seeder file
Examples:
# Create TypeScript seeder
hysteria create:seeder user_seeder
# Create JavaScript seeder
hysteria create:seeder user_seeder -j
# Create in custom directory
hysteria create:seeder user_seeder -s custom/seeders
Seed Command
hysteria seed [options]
Options:
-d, --datasource <path>- Path to SqlDataSource file (required)-s, --seeder-path <paths>- Comma-separated paths to folders or files-c, --tsconfig <path>- Path to tsconfig.json
Examples:
# Run all seeders
hysteria seed -d database/index.ts
# Run specific file
hysteria seed -s database/seeders/1234_users.ts -d database/index.ts
# Run multiple files
hysteria seed -s database/seeders/users.ts,database/seeders/posts.ts -d database/index.ts
# Run all seeders in custom folder
hysteria seed -s custom/seeders -d database/index.ts
Best Practices
1. Make Seeders Idempotent
Seeders should be safe to run multiple times:
async run(): Promise<void> {
// Use INSERT ... ON CONFLICT for PostgreSQL
await this.sqlDataSource.rawQuery(`
INSERT INTO users (email, name) VALUES ('admin@app.com', 'Admin')
ON CONFLICT (email) DO NOTHING
`);
// Or check before inserting
const existing = await this.sqlDataSource.rawQuery(
'SELECT id FROM users WHERE email = ?',
['admin@app.com']
);
if (!existing.length) {
await this.sqlDataSource.from(User).insert({ email: 'admin@app.com', name: 'Admin' });
}
}
2. Use Transactions for Related Data
When seeding related data, use transactions:
async run(): Promise<void> {
await this.sqlDataSource.transaction(async (trx) => {
const user = await trx.sql.rawQuery(
'INSERT INTO users (name) VALUES (?) RETURNING id',
['John Doe']
);
await trx.sql.rawQuery(
'INSERT INTO profiles (user_id, bio) VALUES (?, ?)',
[user[0].id, 'Sample bio']
);
});
}
3. Order Matters
Name seeders with prefixes to control execution order:
database/seeders/
├── 001_users.ts # Run first
├── 002_posts.ts # Then posts
└── 003_comments.ts # Finally comments
Seeders run in alphabetical order based on filename.
4. Separate Development and Production Seeders
Use folders to organize seeders by environment:
database/seeders/
├── production/
│ └── 001_admin_user.ts
└── development/
├── 001_test_users.ts
└── 002_test_posts.ts
Then run appropriate seeders:
# Production
hysteria seed -s database/seeders/production -d database/index.ts
# Development
hysteria seed -s database/seeders/development -d database/index.ts
5. Log Progress
Add helpful logging to track seeder execution:
async run(): Promise<void> {
console.log('Starting user seeder...');
const users = await this.sqlDataSource.from(User).insertMany(/* ... */);
console.log(`✓ Created ${users.length} users`);
const posts = await this.sqlDataSource.from(Post).insertMany(/* ... */);
console.log(`✓ Created ${posts.length} posts`);
console.log('✓ User seeder completed successfully');
}
Common Patterns
Seeding with Foreign Keys
Handle relationships correctly:
async run(): Promise<void> {
// Create users first
const users = await this.sqlDataSource.from(User).insertMany([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]);
// Then create posts with user IDs
await this.sqlDataSource.from(Post).insertMany([
{ title: 'Alice Post', user_id: users[0].id },
{ title: 'Bob Post', user_id: users[1].id }
]);
}
Conditional Seeding
Skip seeding if data already exists:
async run(): Promise<void> {
const userCount = await this.sqlDataSource.from(User).getCount();
if (userCount > 0) {
console.log('Users already exist, skipping seeder');
return;
}
// Seed users...
}
Bulk Insert Performance
For large datasets, use batch inserts:
async run(): Promise<void> {
const batchSize = 1000;
const totalRecords = 10000;
for (let i = 0; i < totalRecords; i += batchSize) {
const batch = Array.from({ length: batchSize }, (_, j) => ({
name: `User ${i + j}`,
email: `user${i + j}@example.com`
}));
await this.sqlDataSource.from(User).insertMany(batch);
console.log(`Inserted batch ${i / batchSize + 1}`);
}
}
Troubleshooting
Seeder Not Found
If your seeder isn't being recognized:
- Check the file extension (
.tsor.js) - Verify the file is in the configured seeders path
- Ensure the class extends
BaseSeeder - Confirm it exports a default class
Connection Already Established
If you see CONNECTION_ALREADY_ESTABLISHED error, your datasource file might be connecting automatically. The seeder runner handles connections, so remove any .connect() calls from your datasource file when using with CLI.
Import Errors
If you have import issues with BaseSeeder:
// ✅ Correct import
import { BaseSeeder } from "hysteria-orm";
// ❌ Wrong - don't import from internal paths
import { BaseSeeder } from "hysteria-orm/dist/sql/seeders/base_seeder";
Next Steps
- Learn about Seeders CLI Options for more seeder commands
- Explore Transactions for atomic seeding
- See CLI Overview for all available commands