Touring what got generated
Read every file the generator dropped and connect them mentally.
Eight files appeared. Now we walk through each one so you understand what the generator did — and how to extend it when the default isn't enough.
1. The model
type Product struct {ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`Name string `gorm:"not null" json:"name"`Price decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"price"`StockQuantity int `gorm:"default:0" json:"stock_quantity"`CreatedAt time.Time `json:"created_at"`UpdatedAt time.Time `json:"updated_at"`}
Standard GORM struct. The tags encode the column type, constraints, and JSON serialization. UUID primary key (you can't guess them; great for IDOR defence).
2. The service
type ProductService struct { db *gorm.DB }func (s *ProductService) Create(in CreateProductInput) (*Product, error) {p := &Product{Name: in.Name, Price: in.Price, StockQuantity: in.StockQuantity}if err := s.db.Create(p).Error; err != nil {return nil, fmt.Errorf("creating product: %w", err)}return p, nil}// List, GetByID, Update, Delete — similar pattern.
3. The handler
func (h *ProductHandler) Create(c *gin.Context) {var in services.CreateProductInputif err := c.ShouldBindJSON(&in); err != nil {respond.Error(c, 422, "VALIDATION_ERROR", err)return}p, err := h.svc.Create(in)if err != nil {respond.Error(c, 500, "INTERNAL_ERROR", err)return}respond.Created(c, p, "Product created")}
Thin handler, calls service, shapes response. Exactly the convention you learnt.
4. The routes injection
The generator edits routes.go to mount the new handler:
products := api.Group("/products")products.Use(middleware.Auth(...)){products.GET("", productHandler.List)products.POST("", productHandler.Create)products.GET("/:id", productHandler.GetByID)products.PUT("/:id", productHandler.Update)products.DELETE("/:id", productHandler.Delete)}
5 + 6. Zod schema + TS type
export const ProductSchema = z.object({name: z.string().min(1),price: z.string().regex(/^\d+\.\d{2}$/),stockQuantity: z.number().int().min(0).default(0),})export type Product = z.infer<typeof ProductSchema> & { id: string }
7. The React Query hook
export function useProducts() {return useQuery({ queryKey: ['products'], queryFn: api.products.list })}export function useCreateProduct() {const qc = useQueryClient()return useMutation({mutationFn: api.products.create,onSuccess: () => qc.invalidateQueries({ queryKey: ['products'] }),})}
8. The admin resource page
export default function ProductsPage() {return defineResource({model: "products",columns: [{ key: "name", label: "Name" },{ key: "price", label: "Price", format: "money" },{ key: "stockQuantity", label: "Stock", format: "number" },],form: [{ name: "name", type: "text", required: true },{ name: "price", type: "money", required: true },{ name: "stockQuantity", type: "number", default: 0 },],})}
That's the Filament-style declaration. No HTML, no form wiring. DataTable + FormBuilder do the rest.
Quick check
Try it
Open three of the eight generated files (your pick) and answer in notes.md:
- Which file?
- What does the first 10 lines do?
- If you were to extend it (e.g., add "is_active" to the model), what would you change?
What's next
One more command for the toolkit: grit sync. The Go side keeps changing; the TypeScript types need to keep up. That's the next (and last) lesson of chapter 4.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub