Scaffold and develop production-ready REST APIs using the Echo v4 web framework with custom validation, JWT auth, Swagger, and idiomatic Go patterns.
77
72%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-go/echo-project-starter/SKILL.mdScaffold and develop production-ready REST APIs using the Echo v4 web framework with custom validation, JWT auth, Swagger, and idiomatic Go patterns.
go version)swag CLI for Swagger generation (go install github.com/swaggo/swag/cmd/swag@latest)golangci-lint for linting (go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)mkdir -p myapp && cd myapp
go mod init github.com/yourorg/myapp
go get github.com/labstack/echo/v4@latest
go get github.com/labstack/echo-jwt/v4@latest
go get github.com/go-playground/validator/v10@latest
go get github.com/swaggo/echo-swagger@latest
go get github.com/swaggo/swag@latest
go get gorm.io/gorm@latest
go get gorm.io/driver/postgres@latestmyapp/
├── cmd/
│ └── server/
│ └── main.go # Entrypoint, wires dependencies
├── internal/
│ ├── config/
│ │ └── config.go # Env-based configuration
│ ├── handler/
│ │ ├── user.go # User HTTP handlers
│ │ ├── auth.go # Auth HTTP handlers
│ │ └── health.go # Health check endpoint
│ ├── middleware/
│ │ └── auth.go # JWT middleware config
│ ├── model/
│ │ └── user.go # Domain/DB models
│ ├── repository/
│ │ └── user.go # Data access layer
│ ├── service/
│ │ └── user.go # Business logic
│ ├── validator/
│ │ └── validator.go # Custom validator registration
│ └── server/
│ └── server.go # Echo instance setup, routes, middleware
├── docs/ # Generated by swag init
├── .env.example # Environment variable template
├── go.mod
├── go.sum
└── Makefileinternal/ to keep application code unexportable.context.Context via c.Request().Context() through service and repository layers.c.Validate() uses go-playground/validator.HTTPErrorHandler for centralized error responses -- do not scatter error formatting across handlers.c.Bind() (auto-detects JSON, form, query), then validate with c.Validate().e.Group() and apply middleware per group.return echo.NewHTTPError(...)) instead of writing responses and returning nil.// cmd/server/main.go
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/server"
)
// @title MyApp API
// @version 1.0
// @description A sample Echo API server.
// @host localhost:8080
// @BasePath /api/v1
func main() {
cfg := config.Load()
srv := server.New(cfg)
go func() {
slog.Info("server starting", "port", cfg.Port)
if err := srv.Start(":" + cfg.Port); err != nil {
slog.Info("server stopped", "reason", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("forced shutdown", "error", err)
}
slog.Info("server exited")
}// internal/validator/validator.go
package validator
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
// CustomValidator wraps go-playground/validator for Echo.
type CustomValidator struct {
validator *validator.Validate
}
func New() *CustomValidator {
return &CustomValidator{validator: validator.New()}
}
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, formatValidationErrors(err))
}
return nil
}
func formatValidationErrors(err error) map[string]string {
errs := make(map[string]string)
for _, e := range err.(validator.ValidationErrors) {
errs[e.Field()] = e.Tag() + " validation failed"
}
return errs
}// internal/server/server.go
package server
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"gorm.io/gorm"
"github.com/yourorg/myapp/internal/config"
"github.com/yourorg/myapp/internal/handler"
appMiddleware "github.com/yourorg/myapp/internal/middleware"
"github.com/yourorg/myapp/internal/repository"
"github.com/yourorg/myapp/internal/service"
appValidator "github.com/yourorg/myapp/internal/validator"
)
func New(cfg *config.Config, db *gorm.DB) *echo.Echo {
e := echo.New()
e.HideBanner = true
// Custom validator
e.Validator = appValidator.New()
// Custom error handler
e.HTTPErrorHandler = customErrorHandler
// Global middleware
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: cfg.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization},
}))
// Swagger
e.GET("/swagger/*", echoSwagger.WrapHandler)
// Health
e.GET("/healthz", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
// API v1
v1 := e.Group("/api/v1")
// Public routes
authHandler := handler.NewAuthHandler(cfg)
v1.POST("/auth/login", authHandler.Login)
v1.POST("/auth/register", authHandler.Register)
// Wire dependencies
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
// Protected routes
protected := v1.Group("")
protected.Use(appMiddleware.JWTMiddleware(cfg.JWTSecret))
userHandler := handler.NewUserHandler(userSvc)
protected.GET("/users", userHandler.List)
protected.GET("/users/:id", userHandler.GetByID)
protected.PUT("/users/:id", userHandler.Update)
protected.DELETE("/users/:id", userHandler.Delete)
return e
}
func customErrorHandler(err error, c echo.Context) {
if c.Response().Committed {
return
}
he, ok := err.(*echo.HTTPError)
if !ok {
he = echo.NewHTTPError(http.StatusInternalServerError, "internal server error")
}
c.JSON(he.Code, map[string]interface{}{
"error": he.Message,
})
}// internal/middleware/auth.go
package middleware
import (
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/golang-jwt/jwt/v5"
)
func JWTMiddleware(secret string) echo.MiddlewareFunc {
return echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(secret),
NewClaimsFunc: func(c echo.Context) jwt.Claims {
return new(jwt.RegisteredClaims)
},
})
}// internal/handler/user.go
package handler
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/yourorg/myapp/internal/service"
)
type UserHandler struct {
svc service.UserService
}
func NewUserHandler(svc service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gte=1,lte=150"`
}
type UpdateUserRequest struct {
Name string `json:"name" validate:"omitempty,min=2,max=100"`
Age int `json:"age" validate:"omitempty,gte=1,lte=150"`
}
// GetByID godoc
// @Summary Get user by ID
// @Tags users
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} model.User
// @Failure 404 {object} map[string]string
// @Router /users/{id} [get]
func (h *UserHandler) GetByID(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid id parameter")
}
ctx := c.Request().Context()
user, err := h.svc.GetByID(ctx, uint(id))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return c.JSON(http.StatusOK, user)
}
// List godoc
// @Summary List users with pagination
// @Tags users
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Page size" default(20)
// @Success 200 {object} map[string]interface{}
// @Router /users [get]
func (h *UserHandler) List(c echo.Context) error {
type PaginationQuery struct {
Page int `query:"page" validate:"omitempty,gte=1"`
PageSize int `query:"page_size" validate:"omitempty,gte=1,lte=100"`
}
q := PaginationQuery{Page: 1, PageSize: 20}
if err := c.Bind(&q); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid query parameters")
}
if err := c.Validate(&q); err != nil {
return err
}
ctx := c.Request().Context()
users, total, err := h.svc.List(ctx, q.Page, q.PageSize)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to list users")
}
return c.JSON(http.StatusOK, map[string]interface{}{
"data": users,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}
func (h *UserHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid id parameter")
}
var req UpdateUserRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}
if err := c.Validate(&req); err != nil {
return err
}
ctx := c.Request().Context()
user, err := h.svc.Update(ctx, uint(id), req.Name, req.Age)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to update user")
}
return c.JSON(http.StatusOK, user)
}
func (h *UserHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid id parameter")
}
ctx := c.Request().Context()
if err := h.svc.Delete(ctx, uint(id)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete user")
}
return c.NoContent(http.StatusNoContent)
}// internal/service/user.go
package service
import (
"context"
"errors"
"github.com/yourorg/myapp/internal/model"
"github.com/yourorg/myapp/internal/repository"
)
var (
ErrNotFound = errors.New("not found")
ErrDuplicateEmail = errors.New("duplicate email")
)
type UserService interface {
Create(ctx context.Context, name, email string, age int) (*model.User, error)
GetByID(ctx context.Context, id uint) (*model.User, error)
List(ctx context.Context, page, pageSize int) ([]model.User, int64, error)
Update(ctx context.Context, id uint, name string, age int) (*model.User, error)
Delete(ctx context.Context, id uint) error
}
type userService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) UserService {
return &userService{repo: repo}
}
func (s *userService) Create(ctx context.Context, name, email string, age int) (*model.User, error) {
existing, _ := s.repo.GetByEmail(ctx, email)
if existing != nil {
return nil, ErrDuplicateEmail
}
user := &model.User{Name: name, Email: email, Age: age}
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id uint) (*model.User, error) {
return s.repo.GetByID(ctx, id)
}
func (s *userService) List(ctx context.Context, page, pageSize int) ([]model.User, int64, error) {
offset := (page - 1) * pageSize
return s.repo.List(ctx, offset, pageSize)
}
func (s *userService) Update(ctx context.Context, id uint, name string, age int) (*model.User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if name != "" {
user.Name = name
}
if age > 0 {
user.Age = age
}
if err := s.repo.Update(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *userService) Delete(ctx context.Context, id uint) error {
return s.repo.Delete(ctx, id)
}.env.example to .env and fill in valuesgo mod downloadmigrate -path migrations -database "$DATABASE_URL" upgo run ./cmd/servercurl http://localhost:8080/healthz# Run the server
go run ./cmd/server
# Run tests
go test ./... -v -race -count=1
# Run tests with coverage
go test ./... -coverprofile=coverage.out && go tool cover -html=coverage.out
# Generate Swagger docs
swag init -g cmd/server/main.go -o docs
# Lint
golangci-lint run ./...
# Build binary
go build -o bin/server ./cmd/server
# Tidy dependencies
go mod tidy
# Format code
gofmt -w .HTTPErrorHandler -- lean into this pattern.echo.New() + httptest.NewRequest + httptest.NewRecorder for handler tests. Set e.Validator in test setup. Mock service interfaces.golang.org/x/net/websocket. Use echo.WrapHandler for gorilla/websocket if needed.golang:1.22-alpine for build, alpine:latest for runtime.echo-contrib for Prometheus metrics middleware. Echo's middleware.Logger() emits structured JSON when configured.c.FormFile() for single files, c.MultipartForm() for multiple. Set e.MaxBodySize to limit upload size.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.