backend-go/echo-project-starter/SKILL.md
Scaffold and develop production-ready REST APIs using the Echo v4 web framework with custom validation, JWT auth, Swagger, and idiomatic Go patterns.
npx skillsauth add achreftlili/deep-dev-skills echo-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold 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@latest
myapp/
├── 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
└── Makefile
internal/ 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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.