Backend

Seeders

Seeders populate your database with initial data — admin accounts, demo users, default categories, or any test data you need during development. Grit scaffolds a ready-to-use seed system with a dedicated command and extensible seeder functions.

Running Seeders

After running migrations, seed your database with initial data:

terminal
$ cd apps/api && go run cmd/seed/main.go

The seed command first ensures all tables exist (by running migrations), then executes each seeder. Seeders are idempotent — running them multiple times won't create duplicate records:

output
Database connected successfully
Running migrations...
All tables are up to date — nothing to migrate.
Seeding database...
Created admin user: admin@example.com / password
Created user: jane@example.com / password
Created user: robert@example.com / password
Created user: emily@example.com / password
Created user: michael@example.com / password
Database seeded successfully.

Default Seeders

Every Grit project ships with two seeders: an admin account and a set of demo users. These are defined in internal/database/seed.go.

Admin User

Creates the default admin account for accessing the admin panel:

apps/api/internal/database/seed.go
func seedAdminUser(db *gorm.DB) error {
var count int64
db.Model(&models.User{}).Where("email = ?", "admin@example.com").Count(&count)
if count > 0 {
log.Println("Admin user already exists, skipping...")
return nil
}
admin := models.User{
Name: "Admin",
Email: "admin@example.com",
Password: "password",
Role: "admin",
Active: true,
}
if err := db.Create(&admin).Error; err != nil {
return fmt.Errorf("creating admin user: %w", err)
}
log.Println("Created admin user: admin@example.com / password")
return nil
}

Important: Change the default admin password immediately in production. The password is hashed via bcrypt in the BeforeCreate hook.

Demo Users

Creates sample user accounts with different roles for testing:

apps/api/internal/database/seed.go
func seedDemoUsers(db *gorm.DB) error {
users := []models.User{
{Name: "Jane Cooper", Email: "jane@example.com", Password: "password", Role: "editor", Active: true},
{Name: "Robert Fox", Email: "robert@example.com", Password: "password", Role: "user", Active: true},
{Name: "Emily Davis", Email: "emily@example.com", Password: "password", Role: "user", Active: true},
{Name: "Michael Chen", Email: "michael@example.com", Password: "password", Role: "user", Active: false},
}
for _, u := range users {
var count int64
db.Model(&models.User{}).Where("email = ?", u.Email).Count(&count)
if count > 0 {
continue
}
if err := db.Create(&u).Error; err != nil {
log.Printf("Warning: failed to create user %s: %v", u.Email, err)
continue
}
log.Printf("Created user: %s / password", u.Email)
}
return nil
}

Blog Posts

Every Grit project includes built-in blog seed data via the seedBlogs() function. This creates 4 sample blog posts (3 published, 1 draft) to demonstrate the blog feature out of the box. Like all seeders, it is idempotent and safe to run multiple times.

apps/api/internal/database/seed.go
func seedBlogs(db *gorm.DB) error {
blogs := []models.Blog{
{Title: "Getting Started with Grit", Slug: "getting-started-with-grit", Status: "published", ...},
{Title: "Building APIs with Go & Gin", Slug: "building-apis-with-go-gin", Status: "published", ...},
{Title: "Admin Panels Made Easy", Slug: "admin-panels-made-easy", Status: "published", ...},
{Title: "Advanced Grit Patterns", Slug: "advanced-grit-patterns", Status: "draft", ...},
}
for _, blog := range blogs {
var count int64
db.Model(&models.Blog{}).Where("slug = ?", blog.Slug).Count(&count)
if count > 0 {
continue
}
if err := db.Create(&blog).Error; err != nil {
log.Printf("Warning: failed to create blog %s: %v", blog.Title, err)
continue
}
log.Printf("Created blog: %s", blog.Title)
}
return nil
}

The blog seeder is automatically called by the Seed() function alongside the admin and demo user seeders. After running go run cmd/seed/main.go, you can immediately visit the blog pages in the web app to see the sample content.

The Seed Entrypoint

The seed command lives at cmd/seed/main.go. It runs migrations first to ensure all tables exist, then executes the Seed() function:

apps/api/cmd/seed/main.go
package main
import (
"fmt"
"log"
"os"
"myapp/apps/api/internal/config"
"myapp/apps/api/internal/database"
"myapp/apps/api/internal/models"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
db, err := database.Connect(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Ensure tables exist before seeding
fmt.Println("Running migrations...")
if err := models.Migrate(db); err != nil {
log.Fatalf("Migration failed: %v", err)
}
fmt.Println("Seeding database...")
if err := database.Seed(db); err != nil {
log.Fatalf("Seeding failed: %v", err)
}
fmt.Println("Database seeded successfully.")
os.Exit(0)
}

Creating Custom Seeders

Add your own seeders by creating new functions and registering them in the Seed() function. Each seeder should be idempotent — safe to run multiple times without creating duplicates.

Step 1: Write the seeder function

apps/api/internal/database/seed.go
func seedCategories(db *gorm.DB) error {
categories := []models.Category{
{Name: "Electronics", Slug: "electronics"},
{Name: "Clothing", Slug: "clothing"},
{Name: "Books", Slug: "books"},
{Name: "Home & Garden", Slug: "home-garden"},
}
for _, cat := range categories {
var count int64
db.Model(&models.Category{}).Where("slug = ?", cat.Slug).Count(&count)
if count > 0 {
continue
}
if err := db.Create(&cat).Error; err != nil {
log.Printf("Warning: failed to create category %s: %v", cat.Name, err)
continue
}
log.Printf("Created category: %s", cat.Name)
}
return nil
}

Step 2: Register it in Seed()

Add your seeder call to the Seed() function. If you used grit generate resource, the generator automatically adds it via the // grit:seeders marker:

apps/api/internal/database/seed.go
func Seed(db *gorm.DB) error {
if err := seedAdminUser(db); err != nil {
return fmt.Errorf("seeding admin user: %w", err)
}
if err := seedDemoUsers(db); err != nil {
return fmt.Errorf("seeding demo users: %w", err)
}
if err := seedCategories(db); err != nil {
return fmt.Errorf("seeding categories: %w", err)
}
// grit:seeders
return nil
}

Best Practices

Follow these patterns when writing seeders:

PatternWhy
Check before creatingPrevents duplicate records when running seeders multiple times
Use unique fields for lookupsCheck by email, slug, or other unique identifiers — not by name
Log what you createMakes it easy to verify seeding worked correctly
Wrap errors with contextUse fmt.Errorf("seeding X: %w", err) for clear error messages
Seed parent records firstIf Products need Categories, seed categories before products

Reset & Reseed

A common development workflow is to drop everything and start fresh with seed data. Combine the fresh migration with seeding:

terminal
$ go run cmd/migrate/main.go --fresh
$ go run cmd/seed/main.go

This drops all tables, recreates them from your models, and populates them with seed data. Perfect for resetting your local development environment.