Docs / fastkit-cli / db seed

db seed

Populate your database with initial or test data using seeders — Python classes that define exactly what gets inserted. Run all seeders at once or target a specific one by name.

Overview

Seeders are useful in several situations:

Development setup

Populate a fresh database with realistic test data so developers can work without manually inserting rows.

Reference data

Insert required lookup data — roles, categories, status codes — that must exist before the app runs.

Demo data

Seed a demo environment with sample users, products, and orders for presentations or staging.

Test isolation

Reset and re-seed a test database to a known state before each test run.

Usage

fastkit db seed [SeederName]

Run all seeders

bash
fastkit db seed
Terminal — run all seeders
$ fastkit db seed
Running all seeders...
✓  RoleSeeder — 4 records
✓  UserSeeder — 10 records
✓  ClientSeeder — 20 records
✓  ProductSeeder — 50 records
Done! 84 records inserted.

Run a specific seeder

bash
fastkit db seed UserSeeder
fastkit db seed ProductSeeder
Terminal — run specific seeder
$ fastkit db seed UserSeeder
Running UserSeeder...
✓  UserSeeder — 10 records
Done! 10 records inserted.

The seeder name must match the class name exactly, including capitalization — UserSeeder not userseeder.

Writing Seeders

Each seeder is a Python class with a run() method. FastKit CLI discovers them automatically from the seeders/ directory in your project.

Basic seeder

python
# seeders/user_seeder.py
from fastkit_core.database import get_db
from modules.users.models import User
from modules.users.schemas import UserCreate
from modules.users.service import UserService

class UserSeeder:
    """Seed initial users."""

    def run(self) -> None:
        session = next(get_db())
        service = UserService(session)

        users = [
            UserCreate(
                name="Alice Admin",
                email="alice@example.com",
                password="Secret1!",
                role="admin"
            ),
            UserCreate(
                name="Bob User",
                email="bob@example.com",
                password="Secret1!",
                role="user"
            ),
        ]

        for user_data in users:
            if not service.exists(email=user_data.email):
                service.create(user_data)

        session.close()

Always check for existing records before inserting — service.exists(email=...) — so seeders are idempotent and safe to run multiple times without creating duplicates.

Seeder with reference data

Reference data — roles, categories, statuses — that must exist before the application runs:

python
# seeders/role_seeder.py
from fastkit_core.database import get_db
from modules.roles.models import Role

class RoleSeeder:
    """Seed application roles — required before UserSeeder."""

    ROLES = [
        {"name": "admin",   "description": "Full access"},
        {"name": "manager", "description": "Manage resources"},
        {"name": "user",    "description": "Standard access"},
        {"name": "guest",   "description": "Read-only access"},
    ]

    def run(self) -> None:
        session = next(get_db())

        for role_data in self.ROLES:
            exists = session.query(Role).filter_by(name=role_data["name"]).first()
            if not exists:
                session.add(Role(**role_data))

        session.commit()
        session.close()

Async seeder

If your project uses async sessions, implement run() as a coroutine:

python
# seeders/product_seeder.py
import asyncio
from fastkit_core.database import get_async_db
from modules.products.service import ProductService
from modules.products.schemas import ProductCreate

class ProductSeeder:
    """Seed sample products — async version."""

    async def run(self) -> None:
        async for session in get_async_db():
            service = ProductService(session)

            products = [
                ProductCreate(name="Widget Pro",   price=29.99, stock=100),
                ProductCreate(name="Gadget Basic", price=9.99,  stock=500),
                ProductCreate(name="Super Bundle", price=49.99, stock=50),
            ]

            for product_data in products:
                if not await service.exists(name=product_data.name):
                    await service.create(product_data)
            break

Project Structure

Place all seeders in a seeders/ directory at the project root. FastKit CLI discovers all classes ending in Seeder automatically:

Project root
seeders/ Seeder discovery directory
__init__.py Optional — for shared seeder utilities
role_seeder.py class RoleSeeder
user_seeder.py class UserSeeder
client_seeder.py class ClientSeeder
product_seeder.py class ProductSeeder

Naming conventions — one seeder class per file, file named in snake_case, class named in PascalCase with a Seeder suffix:

FileClass name
role_seeder.pyRoleSeeder
user_seeder.pyUserSeeder
invoice_seeder.pyInvoiceSeeder

Ordering & Dependencies

When running all seeders with fastkit db seed, the execution order follows the file discovery order (alphabetical by filename). Since seeders often depend on each other — users need roles, invoices need clients — design your filenames to run in the right order:

01_ 01_role_seeder.py No dependencies — runs first
02_ 02_user_seeder.py Needs roles
03_ 03_client_seeder.py Needs users
04_ 04_product_seeder.py Independent
05_ 05_invoice_seeder.py Needs clients & products

With numeric prefixes, the order is explicit and immediately readable. Running a specific seeder by name still works regardless of the prefix:

bash
# Still works — the name is the class name, not the filename
fastkit db seed UserSeeder
fastkit db seed InvoiceSeeder

Best Practices

Make seeders idempotent

Seeders should be safe to run multiple times. Always check before inserting:

❌ Not idempotent
python
def run(self):
    # Inserts duplicates every run
    service.create(UserCreate(
        email="admin@example.com",
        ...
    ))
✓ Idempotent
python
def run(self):
    email = "admin@example.com"
    if not service.exists(email=email):
        service.create(UserCreate(
            email=email, ...
        ))

Keep seeders focused

One seeder per model — don't insert users and roles in the same file. This makes it easy to run or debug individual seeders and keeps the order of execution explicit.

Use realistic data

Test data should resemble real data. Libraries like Faker make it easy to generate names, emails, addresses, and more:

python
from faker import Faker

fake = Faker()

class ClientSeeder:
    def run(self) -> None:
        session = next(get_db())
        service = ClientService(session)

        for _ in range(20):
            service.create(ClientCreate(
                name=fake.company(),
                email=fake.company_email(),
                phone=fake.phone_number(),
                address=fake.address(),
            ))

        session.close()

Never run in production with destructive data

Seeders that truncate tables before inserting are fine in development but dangerous in production. Use environment checks to guard destructive operations:

python
import os

class DemoDataSeeder:
    def run(self) -> None:
        if os.getenv("APP_ENV") == "production":
            print("Skipping DemoDataSeeder in production.")
            return
        # ... insert demo data safely