Quick Start: Adding a Data Model
A simplified guide to add a new data model in 6 easy steps.
This guide shows you how to add a simple Product model with 4 properties. Perfect for beginners!
Estimated time: 10 minutes ⚡
Manual Setup (Learn the Details)
Follow these steps to understand how models are structured in Gojang:
What We're Building
A Product model with:
- Name - product name
- Price - product price
- Stock - quantity available
- Description - product details
We'll create:
- ✅ Database schema
- ✅ CRUD handlers (Create, Read, Update, Delete)
- ✅ Routes
- ✅ Templates (views)
- ✅ Admin panel integration
Step 1: Create the Schema
Create a new file app/schema/product.go:
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Product struct {
ent.Schema
}
func (Product) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Float("price").
Positive(),
field.Int("stock").
Default(0),
field.Text("description").
Optional(),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}
Step 2: Generate Database Code
Run these commands:
go generate ./app/gojang/models
This creates all the database code automatically.
Step 3: Create Form Validation
Add to app/views/forms/forms.go:
type ProductForm struct {
Name string `form:"name" validate:"required"`
Price float64 `form:"price" validate:"required,gt=0"`
Stock int `form:"stock" validate:"gte=0"`
Description string `form:"description"`
}
Step 4: Create Handler
Create app/products/products.handler.go:
package products
import (
"log"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/gojangframework/gojang/app/gojang/models"
"github.com/gojangframework/gojang/app/views/forms"
"github.com/gojangframework/gojang/app/gojang/views/renderers"
)
type ProductHandler struct {
Client *models.Client
Renderer *renderers.Renderer
}
func NewProductHandler(client *models.Client, renderer *renderers.Renderer) *ProductHandler {
return &ProductHandler{
Client: client,
Renderer: renderer,
}
}
// Index - List all products
func (h *ProductHandler) Index(w http.ResponseWriter, r *http.Request) {
products, err := h.Client.Product.Query().
Order(models.Desc("created_at")).
All(r.Context())
if err != nil {
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to load products")
return
}
h.Renderer.Render(w, r, "products/index.html", &renderers.TemplateData{
Title: "Products",
Data: map[string]interface{}{
"Products": products,
},
})
}
// New - Show form to create product
func (h *ProductHandler) New(w http.ResponseWriter, r *http.Request) {
h.Renderer.Render(w, r, "products/new.partial.html", &renderers.TemplateData{
Title: "New Product",
})
}
// Create - Save new product
func (h *ProductHandler) Create(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, "Invalid form")
return
}
var form forms.ProductForm
if err := forms.Decode(r, &form); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := forms.Validate(r.Context(), &form); err != nil {
h.Renderer.Render(w, r, "products/new.partial.html", &renderers.TemplateData{
Title: "New Product",
Data: map[string]interface{}{
"Form": form,
"Errors": err,
},
})
return
}
_, err := h.Client.Product.Create().
SetName(form.Name).
SetPrice(form.Price).
SetStock(form.Stock).
SetDescription(form.Description).
Save(r.Context())
if err != nil {
log.Printf("Error creating product: %v", err)
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to create product")
return
}
http.Redirect(w, r, "/products", http.StatusSeeOther)
}
// Edit - Show form to edit product
func (h *ProductHandler) Edit(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
product, err := h.Client.Product.Get(r.Context(), id)
if err != nil {
h.Renderer.RenderError(w, r, http.StatusNotFound, "Product not found")
return
}
h.Renderer.Render(w, r, "products/edit.partial.html", &renderers.TemplateData{
Title: "Edit Product",
Data: map[string]interface{}{
"Product": product,
},
})
}
// Update - Save changes to product
func (h *ProductHandler) Update(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
if err := r.ParseForm(); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, "Invalid form")
return
}
var form forms.ProductForm
if err := forms.Decode(r, &form); err != nil {
h.Renderer.RenderError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := forms.Validate(r.Context(), &form); err != nil {
product, _ := h.Client.Product.Get(r.Context(), id)
h.Renderer.Render(w, r, "products/edit.partial.html", &renderers.TemplateData{
Title: "Edit Product",
Data: map[string]interface{}{
"Product": product,
"Form": form,
"Errors": err,
},
})
return
}
_, err := h.Client.Product.UpdateOneID(id).
SetName(form.Name).
SetPrice(form.Price).
SetStock(form.Stock).
SetDescription(form.Description).
Save(r.Context())
if err != nil {
log.Printf("Error updating product: %v", err)
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to update product")
return
}
http.Redirect(w, r, "/products", http.StatusSeeOther)
}
// Delete - Remove product
func (h *ProductHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
if err := h.Client.Product.DeleteOneID(id).Exec(r.Context()); err != nil {
log.Printf("Error deleting product: %v", err)
h.Renderer.RenderError(w, r, http.StatusInternalServerError, "Failed to delete product")
return
}
http.Redirect(w, r, "/products", http.StatusSeeOther)
}
Step 5: Create Routes
Create app/products/products.route.go:
package products
import (
"github.com/alexedwards/scs/v2"
"github.com/go-chi/chi/v5"
"github.com/gojangframework/gojang/app/gojang/http/middleware"
"github.com/gojangframework/gojang/app/gojang/models"
)
func ProductRoutes(handler *ProductHandler, sm *scs.SessionManager, client *models.Client) chi.Router {
r := chi.NewRouter()
// Public routes
r.Get("/", handler.Index)
// Protected routes (require login)
r.Group(func(auth chi.Router) {
auth.Use(middleware.RequireAuth(sm, client))
auth.Get("/new", handler.New)
auth.Post("/", handler.Create)
auth.Get("/{id}/edit", handler.Edit)
auth.Put("/{id}", handler.Update)
auth.Delete("/{id}", handler.Delete)
})
return r
}
Register in Main
Edit app/cmd/web/main.go and add these lines where other routes are registered:
// Add this import:
// "github.com/gojangframework/gojang/app/products"
// Find this section (around line 150):
productHandler := products.NewProductHandler(client, publicRenderer)
r.Mount("/products", products.ProductRoutes(productHandler, sessionManager, client))
Step 6: Create Templates
Create directory:
mkdir -p app/products/templates
Create app/products/templates/index.html:
{{define "title"}}Products{{end}}
{{define "content"}}
<div class="page-header">
<h1>Products</h1>
{{if .User}}
<a href="/products/new"
hx-get="/products/new"
hx-target="#modal-content"
hx-swap="innerHTML"
class="btn btn-primary">
Add Product
</a>
{{end}}
</div>
{{if .Data.Products}}
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
<th>Description</th>
{{if .User}}<th>Actions</th>{{end}}
</tr>
</thead>
<tbody>
{{range .Data.Products}}
<tr>
<td>{{.Name}}</td>
<td>${{printf "%.2f" .Price}}</td>
<td>{{.Stock}}</td>
<td>{{.Description}}</td>
{{if $.User}}
<td class="actions">
<a href="/products/{{.ID}}/edit"
hx-get="/products/{{.ID}}/edit"
hx-target="#modal-content"
hx-swap="innerHTML"
class="btn btn-sm btn-primary">
Edit
</a>
<form method="POST" action="/products/{{.ID}}" style="display: inline;">
<input type="hidden" name="_method" value="DELETE">
<button type="submit"
onclick="return confirm('Delete this product?')"
class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
{{end}}
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p>No products found.</p>
{{end}}
{{end}}
Create app/products/templates/new.partial.html:
{{define "title"}}New Product{{end}}
{{define "content"}}
<div class="modal-header">
<h2>New Product</h2>
</div>
<form method="POST" action="/products" hx-post="/products" hx-swap="none">
{{if .Data.Errors}}
<div class="alert alert-danger">
{{range .Data.Errors}}
<p>{{.}}</p>
{{end}}
</div>
{{end}}
<div class="form-group">
<label for="name">Name</label>
<input type="text"
id="name"
name="name"
value="{{if .Data.Form}}{{.Data.Form.Name}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="price">Price</label>
<input type="number"
id="price"
name="price"
step="0.01"
value="{{if .Data.Form}}{{.Data.Form.Price}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="stock">Stock</label>
<input type="number"
id="stock"
name="stock"
value="{{if .Data.Form}}{{.Data.Form.Stock}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description"
name="description"
rows="3"
class="form-control">{{if .Data.Form}}{{.Data.Form.Description}}{{end}}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Product</button>
<button type="button" onclick="closeModal()" class="btn btn-secondary">Cancel</button>
</div>
</form>
{{end}}
Create app/products/templates/edit.partial.html:
{{define "title"}}Edit Product{{end}}
{{define "content"}}
<div class="modal-header">
<h2>Edit Product</h2>
</div>
<form method="POST" action="/products/{{.Data.Product.ID}}" hx-put="/products/{{.Data.Product.ID}}" hx-swap="none">
{{if .Data.Errors}}
<div class="alert alert-danger">
{{range .Data.Errors}}
<p>{{.}}</p>
{{end}}
</div>
{{end}}
<div class="form-group">
<label for="name">Name</label>
<input type="text"
id="name"
name="name"
value="{{if .Data.Form}}{{.Data.Form.Name}}{{else}}{{.Data.Product.Name}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="price">Price</label>
<input type="number"
id="price"
name="price"
step="0.01"
value="{{if .Data.Form}}{{.Data.Form.Price}}{{else}}{{.Data.Product.Price}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="stock">Stock</label>
<input type="number"
id="stock"
name="stock"
value="{{if .Data.Form}}{{.Data.Form.Stock}}{{else}}{{.Data.Product.Stock}}{{end}}"
required
class="form-control">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description"
name="description"
rows="3"
class="form-control">{{if .Data.Form}}{{.Data.Form.Description}}{{else}}{{.Data.Product.Description}}{{end}}</textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Update Product</button>
<button type="button" onclick="closeModal()" class="btn btn-secondary">Cancel</button>
</div>
</form>
{{end}}
Step 7: Confirm Admin Auto-Discovery
After go generate ./app/gojang/models, the generated Product client is
available on *models.Client, so the admin panel discovers it automatically.
No app/gojang/admin/models.go change is required for a plain resource.
Add a registry.RegisterModel(ModelRegistration{...}) override only when you
need custom admin behavior, such as a different icon/name, curated list fields,
hidden or readonly fields, hooks, custom fields, or eager-loaded relationships.
That's it! The admin panel automatically handles CRUD operations at
/admin/t/product.
Step 8: Test It!
Restart the server:
go run ./app/cmd/webVisit the products page:
- Public: http://localhost:8080/products
- Admin: http://localhost:8080/admin/t/product
Create a product:
- Log in
- Click "Add Product"
- Fill the form
- Click "Create Product"
Next Steps
Want to learn more?
- Add relationships - Connect products to categories or users
- Add images - Upload product photos
- Add pagination - Handle large lists
- Add search - Filter products
- Add validation - Custom validation rules
See the comprehensive guide for advanced features.
Quick Checklist
When adding a new model:
- Create schema in
app/schema/ - Run
go generate ./app/gojang/models - Add form struct to
app/views/forms/forms.go - Create handler in
app/products/products.handler.go - Create routes in
app/products/products.route.go - Register routes in
app/cmd/web/main.go - Create templates in
app/products/templates/ - Add an admin override in
app/gojang/admin/models.goonly if needed - Test CRUD operations
Troubleshooting
Schema changes not applied?
go generate ./app/gojang/models
Templates not found?
- Check files exist in
app/products/templates/ - Restart the server
404 error?
- Check routes are registered in
main.go - Restart the server
Form validation not working?
- Check form struct has correct tags
- Check field names match HTML inputs
Need help? See the full documentation or README.