Develop Your First Feature
This guide walks you through developing your first feature in Bingo using the code generation tools and following best practices.
Example: Building a Blog Post Module
We'll create a complete CRUD feature for blog posts from start to finish.
Step 1: Generate the Basic Code
Use bingo to generate the complete code skeleton:
bash
# Generate CRUD code for post module
bingo make crud postThis automatically generates:
internal/pkg/model/post.go- Data modelinternal/apiserver/store/post.go- Data access layerinternal/apiserver/biz/post/post.go- Business logic layerinternal/apiserver/controller/v1/post/post.go- HTTP handlerpkg/api/v1/post.go- API request/response definitions
Step 2: Define the Data Model
Edit internal/pkg/model/post.go:
go
package model
import "time"
type Post struct {
ID string `gorm:"primaryKey"`
Title string `gorm:"index"`
Content string `gorm:"type:longtext"`
Author string
Status string // draft, published, archived
ViewCount int64
CreatedAt time.Time
UpdatedAt time.Time
}
// Implement TableName for GORM
func (Post) TableName() string {
return "posts"
}Step 3: Implement Data Access Layer (Store)
Edit internal/apiserver/store/post.go:
go
package store
import (
"context"
"github.com/myorg/myapp/internal/pkg/model"
"gorm.io/gorm"
)
type PostStore interface {
CreatePost(ctx context.Context, post *model.Post) error
GetPostByID(ctx context.Context, id string) (*model.Post, error)
ListPosts(ctx context.Context, status string, limit int, offset int) ([]*model.Post, error)
UpdatePost(ctx context.Context, id string, post *model.Post) error
DeletePost(ctx context.Context, id string) error
}
type postStore struct {
db *gorm.DB
}
func NewPostStore(db *gorm.DB) PostStore {
return &postStore{db: db}
}
func (s *postStore) CreatePost(ctx context.Context, post *model.Post) error {
return s.db.WithContext(ctx).Create(post).Error
}
func (s *postStore) GetPostByID(ctx context.Context, id string) (*model.Post, error) {
var post model.Post
err := s.db.WithContext(ctx).First(&post, "id = ?", id).Error
return &post, err
}
func (s *postStore) ListPosts(ctx context.Context, status string, limit int, offset int) ([]*model.Post, error) {
var posts []*model.Post
query := s.db.WithContext(ctx)
if status != "" {
query = query.Where("status = ?", status)
}
err := query.Limit(limit).Offset(offset).Find(&posts).Error
return posts, err
}Step 4: Implement Business Logic Layer (Biz)
Edit internal/apiserver/biz/post/post.go:
go
package post
import (
"context"
"fmt"
"time"
"github.com/myorg/myapp/internal/pkg/model"
"github.com/myorg/myapp/internal/apiserver/store"
)
type PostBiz interface {
CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error)
GetPost(ctx context.Context, id string) (*Post, error)
ListPosts(ctx context.Context, status string, limit, offset int) ([]*Post, error)
PublishPost(ctx context.Context, id string) error
DeletePost(ctx context.Context, id string) error
}
type postBiz struct {
store store.PostStore
}
type CreatePostRequest struct {
Title string
Content string
Author string
}
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Status string `json:"status"`
ViewCount int64 `json:"view_count"`
CreatedAt time.Time `json:"created_at"`
}
func NewPostBiz(s store.PostStore) PostBiz {
return &postBiz{store: s}
}
func (b *postBiz) CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error) {
// Validate input
if req.Title == "" {
return nil, fmt.Errorf("title is required")
}
// Create model
post := &model.Post{
ID: generateID(),
Title: req.Title,
Content: req.Content,
Author: req.Author,
Status: "draft",
}
// Save to database
if err := b.store.CreatePost(ctx, post); err != nil {
return nil, fmt.Errorf("failed to create post: %w", err)
}
return b.modelToDTO(post), nil
}
func (b *postBiz) PublishPost(ctx context.Context, id string) error {
post, err := b.store.GetPostByID(ctx, id)
if err != nil {
return err
}
post.Status = "published"
return b.store.UpdatePost(ctx, id, post)
}
func (b *postBiz) modelToDTO(post *model.Post) *Post {
return &Post{
ID: post.ID,
Title: post.Title,
Content: post.Content,
Author: post.Author,
Status: post.Status,
ViewCount: post.ViewCount,
CreatedAt: post.CreatedAt,
}
}Step 5: Implement HTTP Handler (Controller)
Edit internal/apiserver/controller/v1/post/post.go:
go
package post
import (
"github.com/gin-gonic/gin"
"github.com/myorg/myapp/internal/apiserver/biz/post"
)
type PostController struct {
biz post.PostBiz
}
func NewPostController(b post.PostBiz) *PostController {
return &PostController{biz: b}
}
// Create godoc
// @Summary Create a new post
// @Tags Posts
// @Accept json
// @Produce json
// @Param post body CreatePostRequest true "Post data"
// @Success 201 {object} post.Post
// @Router /v1/posts [post]
func (c *PostController) CreatePost(ctx *gin.Context) {
var req post.CreatePostRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
p, err := c.biz.CreatePost(ctx, &req)
if err != nil {
ctx.JSON(500, gin.H{"error": err.Error()})
return
}
ctx.JSON(201, p)
}
// GetPost godoc
// @Summary Get a post
// @Tags Posts
// @Produce json
// @Param id path string true "Post ID"
// @Success 200 {object} post.Post
// @Router /v1/posts/{id} [get]
func (c *PostController) GetPost(ctx *gin.Context) {
id := ctx.Param("id")
p, err := c.biz.GetPost(ctx, id)
if err != nil {
ctx.JSON(404, gin.H{"error": "post not found"})
return
}
ctx.JSON(200, p)
}Step 6: Register Routes
Update internal/apiserver/router/api.go:
go
package router
import (
"github.com/gin-gonic/gin"
postctrl "github.com/myorg/myapp/internal/apiserver/controller/v1/post"
)
func RegisterRoutes(engine *gin.Engine, postController *postctrl.PostController) {
v1 := engine.Group("/v1")
// Post routes
posts := v1.Group("/posts")
{
posts.POST("", postController.CreatePost)
posts.GET("/:id", postController.GetPost)
posts.PUT("/:id", postController.UpdatePost)
posts.DELETE("/:id", postController.DeletePost)
posts.GET("", postController.ListPosts)
}
}Step 7: Run and Test
bash
# Build the project
make build
# Run the service
./_output/platforms/<os>/<arch>/myapp-apiserver
# Test creating a post
curl -X POST http://localhost:8080/v1/posts \
-H "Content-Type: application/json" \
-d '{
"title": "Hello World",
"content": "This is my first post",
"author": "John Doe"
}'
# Test getting the post
curl http://localhost:8080/v1/posts/{post_id}Best Practices Demonstrated
- Separation of Concerns: Clear layer responsibilities
- Error Handling: Proper error propagation and handling
- Input Validation: Validate data at the boundary
- Type Safety: Use strong typing with structs
- Testing: Write unit tests for each layer
- Documentation: Use Swagger comments for API docs
Testing Your Feature
Create internal/apiserver/biz/post/post_test.go:
go
package post
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/myorg/myapp/internal/pkg/model"
)
type mockStore struct{}
func (m *mockStore) CreatePost(ctx context.Context, post *model.Post) error {
return nil
}
func TestCreatePost(t *testing.T) {
store := &mockStore{}
biz := NewPostBiz(store)
req := &CreatePostRequest{
Title: "Test Post",
Content: "Test Content",
Author: "Test Author",
}
post, err := biz.CreatePost(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, "Test Post", post.Title)
}Next Step
- Using Bingo CLI - Boost development efficiency with the code generator