Kashvi

Docs

K
Guide

Complete CRUD Walkthrough

Build a Product API end-to-end: model → DTO → migration → repository → service → controller → auth → seed → test. Follow along step by step or jump to the section you need.

What we're building

By the end of this guide you will have a working REST API for managing products, with these endpoints:

MethodEndpointAuth?Action
GET/api/productsNoList all products
GET/api/products/{id}NoGet one product
POST/api/productsJWTCreate a product
PUT/api/products/{id}JWTUpdate a product
DELETE/api/products/{id}JWTDelete a product

How the layers fit together

Kashvi follows MVC with an extra Repository layer to keep all database calls in one place. Here is the flow for a POST /api/products request:

HTTP Request  →  Router  →  Middleware (JWT, Logger)
                               ↓
                         Controller
                           • ctx.BindJSON(&dto)   ← parse + validate body
                           • repo.Create(&model)  ← call repository
                           • ctx.Created(model)   ← send 201 JSON response
                               ↓
                         Repository
                           • orm.DB().Create(...)  ← the ONLY place that
                                                      touches the database
                               ↓
                           Database (SQLite / MySQL / PostgreSQL)

Step 1 — Generate the resource

From your project root (where go.mod lives), run one command to scaffold every file you need:

kashvi make:resource Product

The CLI prints the exact route snippet to paste into your routes file, and creates the following files:

├── 📁app/
├── 📁models/
│ │ └── 🔷product.goProduct struct with gorm/json/validate tags
├── 📁dto/
│ │ └── 🔷product_dto.goCreateProductRequest, UpdateProductRequest
├── 📁repositories/
│ │ └── 🔷product.goFindByID, All, Create, Update, Delete, Query
├── 📁services/
│ │ └── 🔷product_service.goBusiness logic (optional)
└── 📁controllers/
└── 🔷product_controller.goHTTP handlers using repository + DTOs
├── 📁database/
├── 📁migrations/
│ │ └── 🔷..._create_products_table.goAutoMigrate up, DropTable down
└── 📁seeders/
└── 🔷product_seeder.goSample data via app.RegisterSeeder
└── 📁testdata/
└── 📋product_scenarios.jsonJSON test scenarios: list, create, get, update, delete
All highlighted files are generated automatically — you only need to edit them to add your fields and business logic.

Step 2 — Define the model

Edit app/models/product.go. Add your fields with gorm, json, and validate tags. The validate tags are read automatically by ctx.BindJSON — no extra code needed.

app/models/product.go
package models

import "gorm.io/gorm"

type Product struct {
    gorm.Model
    Name        string  `json:"name"        gorm:"not null"              validate:"required,min=1,max=255"`
    Description string  `json:"description"`
    Price       float64 `json:"price"        gorm:"not null"              validate:"required,gte=0"`
    SKU         string  `json:"sku"         gorm:"uniqueIndex;not null"   validate:"required"`
}

Common validate rules: required, email, min, max, gte, lte, in, url. A failed rule returns 422 Unprocessable Entity with field-level errors — your controller does not need to handle it.

Step 3 — Run the migration

The generated migration file at database/migrations/..._create_products_table.go already contains:

  • Up: db.AutoMigrate(&models.Product) — creates or updates the products table to match your model struct.
  • Down: db.Migrator().DropTable("products") — removes the table on rollback.

Imports use your module path from go.mod. You only need to edit the migration file if you want to add custom SQL indexes or constraints beyond what GORM handles automatically.

kashvi migrate

Step 4 — Repository (data layer)

The generated app/repositories/product.go embeds repository.Base[models.Product] and gives you these methods for free:

MethodDescription
FindByID(id uint)Fetch one record by primary key
All()Fetch all records
Create(m *Product)Insert a new record
Update(m *Product)Save changes to an existing record
Delete(id uint)Soft-delete by primary key
Query()Raw GORM chain for custom filters, pagination, joins

No changes needed for basic CRUD. To add a custom query, add a method to this file:

app/repositories/product.go — custom method
// Optional: add custom queries
func (r *ProductRepository) FindBySKU(sku string) (*models.Product, error) {
    var p models.Product
    err := r.Query().Where("sku = ?", sku).First(&p)
    if err != nil {
        return nil, err
    }
    return &p, nil
}

Step 5 — Service (optional)

The service layer is optional for simple CRUD. Use it when you have business logic that spans multiple repositories, sends emails, publishes queue jobs, etc. The generated service already holds a repository reference:

app/services/product_service.go
func (s *ProductService) CreateProduct(input *models.Product) error {
    if err := s.repo.Create(input); err != nil {
        return err
    }
    // e.g. logger.Info("product_created", "id", input.ID)
    return nil
}

For simple CRUD, the controller can call the repository directly and you can skip the service entirely.

Step 6 — DTOs (request / response)

The generated app/dto/product_dto.go holds:

  • CreateProductRequest — all required fields with validate tags
  • UpdateProductRequest — same fields as pointers (so partial updates work; a missing field means "don't change it")
  • ProductResponse (optional) — a safe shape to return to API clients, hiding internal fields

Controllers bind the request body to these structs with ctx.BindJSON so validation runs before the model is touched. To generate DTOs without a full resource: kashvi make:dto Product.

Step 7 — Controller and validation

The generated controller uses the repository and DTOs — there is no orm import in the controller file. Each handler follows the same pattern:

  • Store: binds JSON to dto.CreateProductRequest, then maps into models.Product and calls repo.Create.
  • Update: loads by ID via repo, binds dto.UpdateProductRequest, applies pointer fields, then repo.Update.
  • Index / Show / Destroy: call repo.All(), repo.FindByID, repo.Delete.

To add logging (recommended for create/update/delete):

app/controllers/product_controller.go — Store
import "github.com/shashiranjanraj/kashvi/pkg/logger"

func (c *ProductController) Store(ctx *appctx.Context) {
    var input dto.CreateProductRequest
    if !ctx.BindJSON(&input) {
        return // BindJSON already sent 422 + validation errors
    }
    model := &models.Product{
        Name: input.Name, Description: input.Description, Price: input.Price, SKU: input.SKU,
    }
    if err := c.repo.Create(model); err != nil {
        ctx.Error(http.StatusBadRequest, "Failed to create product")
        return
    }
    log := logger.WithCtx(ctx.R.Context())
    log.Info("product_created", "id", model.ID, "sku", model.SKU)
    ctx.Created(model)
}

Step 8 — Routes and authentication

Register routes in main.go or app/routes/api.go. The CLI printed the exact snippet when you ran kashvi make:resource — paste it here:

app/routes/api.go
repo := repositories.NewProductRepository()
svc := services.NewProductService(repo)
ctrl := controllers.NewProductController(repo)

api := r.Group("/api")

// Public
api.Get("/products", "products.index", ctx.Wrap(ctrl.Index))
api.Get("/products/{id}", "products.show", ctx.Wrap(ctrl.Show))

// Protected (JWT required). Import: "github.com/shashiranjanraj/kashvi/pkg/middleware"
protected := api.Group("", middleware.AuthMiddleware)
protected.Post("/products", "products.store", ctx.Wrap(ctrl.Store))
protected.Put("/products/{id}", "products.update", ctx.Wrap(ctrl.Update))
protected.Delete("/products/{id}", "products.destroy", ctx.Wrap(ctrl.Destroy))

Step 9 — Seed sample data

Edit database/seeders/product_seeder.go and add some sample rows:

database/seeders/product_seeder.go
func init() {
    app.RegisterSeeder("products", func() {
        database.DB.Create(&[]models.Product{
            {Name: "Laptop", Description: "Gaming", Price: 999.99, SKU: "LAPTOP001"},
            {Name: "Mouse", Description: "Wireless", Price: 29.99, SKU: "MOUSE001"},
        })
    })
}

Ensure migrations and seeders are registered in main.go via blank imports:

main.go — blank imports
import (
    _ "yourmodule/database/migrations"
    _ "yourmodule/database/seeders"
)

Step 10 — Run migrations, seed, and serve

kashvi migrate   # create / update the products table
kashvi seed      # insert sample data
kashvi serve     # start the server on APP_PORT (default 8080)

Set LOG_LEVEL and DB_LOG_MODE in .env (see Logging) to see app logs and SQL queries during development.

Step 11 — Test the API

Quick check with curl

# List all products
curl http://localhost:8080/api/products

# Create a product (no auth on this example)
curl -X POST http://localhost:8080/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Laptop","description":"Gaming","price":999.99,"sku":"LAPTOP001"}'

# Get one product
curl http://localhost:8080/api/products/1

# Update
curl -X PUT http://localhost:8080/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Gaming Laptop","description":"Gaming","price":1099.99,"sku":"LAPTOP001"}'

# Delete
curl -X DELETE http://localhost:8080/api/products/1

Automated with TestKit

The generated testdata/product_scenarios.json defines scenarios (list, create, get, update, delete). Build your app's handler and run:

go
func TestProductAPI(t *testing.T) {
    handler := app.New().
        Routes(RegisterRoutes).
        Handler()
    testkit.RunDir(t, handler, "testdata")
}

Put request/response JSON fixtures in testdata/ as referenced by the scenario file (e.g. product_create_req.json, product_create_res.json). See the TestKit docs for the full scenario format.

Layer quick reference

LayerFileResponsibility
Modelapp/models/product.goStruct + gorm/json/validate tags
DTOapp/dto/product_dto.goRequest/response structs; validate tags; used with ctx.BindJSON
Migrationdatabase/migrations/…AutoMigrate / DropTable for the model
Repositoryapp/repositories/product.goALL DB access (FindByID, All, Create, Update, Delete, Query)
Serviceapp/services/product_service.goOptional business logic using repository
Controllerapp/controllers/product_controller.goHTTP only: BindJSON (validation), call repo/service, log, respond
Routesapp/routes/api.goWire controller methods to HTTP methods + paths
Seederdatabase/seeders/product_seeder.goapp.RegisterSeeder("key", func() { ... })
Teststestdata/product_scenarios.jsontestkit + JSON scenarios

What's next?

See also: Routing, Validation, Authentication, ORM & Database, Migrations & Seeders.