| name | api-development |
| description | ZGO API development standards including pagination, error handling, and RESTful design |
| version | 1.0.0 |
| category | development |
| tags | ["api","rest","pagination","errors","standards"] |
| author | ZGO Team |
| updated | "2026-01-24T00:00:00.000Z" |
API Development Standards
📋 Purpose
This skill provides comprehensive API development standards for the ZGO Go backend project, ensuring consistent and high-quality REST APIs across all modules.
🎯 When to Use
- Creating new API endpoints
- Reviewing API code
- Implementing list/collection endpoints
- Handling errors in handlers
- Designing RESTful resources
⚙️ Prerequisites
📚 Core Standards
1. Pagination (MANDATORY)
Rule: All list/collection endpoints MUST implement pagination.
Why Pagination is Required
- Performance: Prevents loading thousands of records into memory
- User Experience: Faster response times
- Scalability: Database and network efficiency
- API Consistency: Uniform behavior across all list endpoints
Implementation
package user
import (
"github.com/gin-gonic/gin"
"github.com/zgiai/zgo/pkg/handler"
"github.com/zgiai/zgo/pkg/response"
"github.com/zgiai/zgo/pkg/pagination"
"github.com/zgiai/zgo/internal/domain"
)
func (h *Handler) List(c *gin.Context) {
users, paginator, err := pagination.PaginateFromContext[*domain.User](c, h.db)
if err != nil {
response.HandleError(c, "Failed to fetch users", err)
return
}
response.Success(c, paginator)
}
func (h *Handler) List(c *gin.Context) {
var users []User
h.db.Find(&users)
response.Success(c, users)
}
Pagination with Filters
func (h *Handler) List(c *gin.Context) {
query := h.db.Model(&UserPO{})
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
if search := c.Query("search"); search != "" {
query = query.Where("username LIKE ? OR email LIKE ?",
"%"+search+"%", "%"+search+"%")
}
users, paginator, err := pagination.PaginateFromContext[*domain.User](c, query)
if err != nil {
response.HandleError(c, "Failed to fetch users", err)
return
}
response.Success(c, paginator)
}
Pagination Standards
| Setting | Value | Description |
|---|
| Default Page Size | 20 | Records per page if not specified |
| Maximum Page Size | 100 | Upper limit to prevent abuse |
| Query Parameter | page | Page number (1-indexed) |
| Query Parameter | page_size | Records per page |
Example Request:
GET /api/users?page=2&page_size=20&status=active&search=john
Example Response:
{
"code": 0,
"message": "success",
"data": [
{
"id": 21,
"username": "john_doe",
"email": "john@example.com",
"status": "active"
}
],
"meta": {
"total": 150,
"page": 2,
"page_size": 20,
"total_pages": 8
},
"links": {
"first": "/api/users?page=1&page_size=20",
"last": "/api/users?page=8&page_size=20",
"prev": "/api/users?page=1&page_size=20",
"next": "/api/users?page=3&page_size=20"
}
}
2. Unified Error Responses (MANDATORY)
Rule: All error responses MUST use pkg/response package functions.
Why Unified Errors
- Consistency: Same format across all APIs
- Automatic Mapping: Framework handles status code logic
- User-Friendly: Clean error messages for clients
- Debugging: Structured errors for logging
Available Error Functions
import "github.com/zgiai/zgo/pkg/response"
response.BadRequest(c, "message", err)
response.Unauthorized(c)
response.Forbidden(c)
response.NotFound(c, "message", err)
response.Conflict(c, "message", err)
response.UnprocessableEntity(c, "message", err)
response.ValidationFailed(c, fieldErrors)
response.InternalServerError(c, "message", err)
response.ServiceUnavailable(c)
response.HandleError(c, "message", err)
Error Handling Pattern
func (h *Handler) Get(c *gin.Context) {
id, ok := handler.ParseID(c, "id")
if !ok {
return
}
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
response.HandleError(c, "User not found", err)
return
}
response.Success(c, ToResponse(user))
}
Error Response Formats
Simple Error:
{
"code": 404,
"message": "User not found",
"error": "record not found"
}
Validation Error:
{
"code": 422,
"message": "Validation failed",
"errors": {
"email": ["The email field is required", "Email format is invalid"],
"password": ["The password must be at least 8 characters"]
}
}
Custom Error Types
Define module-specific errors in service.go:
package user
import "errors"
var (
ErrUserNotFound = errors.New("user not found")
ErrDuplicateEmail = errors.New("email already exists")
ErrInvalidPassword = errors.New("invalid password")
ErrAccountSuspended = errors.New("account is suspended")
)
func (s *service) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
user, err := s.repo.GetByEmail(ctx, email)
if err != nil {
return nil, ErrUserNotFound
}
if user.Status == "suspended" {
return nil, ErrAccountSuspended
}
return user, nil
}
func (h *Handler) Login(c *gin.Context) {
var req LoginRequest
if !handler.BindJSON(c, &req) {
return
}
user, err := h.service.GetByEmail(c.Request.Context(), req.Email)
if err != nil {
response.HandleError(c, "Login failed", err)
return
}
}
3. Success Responses (MANDATORY)
Rule: Use pkg/response for all successful responses.
response.Success(c, data)
response.Success(c, paginator)
response.Created(c, newUser)
response.NoContent(c)
response.Accepted(c, task)
Success Response Format
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"username": "john_doe",
"email": "john@example.com"
}
}
4. HTTP Method Standards (MANDATORY)
| Method | Usage | Response | Body | Example |
|---|
| GET | Retrieve resource(s) | 200 + data | No | GET /api/users/:id |
| POST | Create new resource | 201 + data | Yes | POST /api/users |
| PATCH | Partial update | 200 + data | Yes | PATCH /api/users/:id |
| PUT | Full replacement | 200 + data | Yes | PUT /api/users/:id |
| DELETE | Remove resource | 204 (no body) | No | DELETE /api/users/:id |
Implementation Examples
func (h *Handler) Get(c *gin.Context) {
id, ok := handler.ParseID(c, "id")
if !ok { return }
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
response.HandleError(c, "User not found", err)
return
}
response.Success(c, ToResponse(user))
}
func (h *Handler) Create(c *gin.Context) {
var req CreateUserRequest
if !handler.BindJSON(c, &req) { return }
user, err := h.service.Create(c.Request.Context(), &req)
if err != nil {
response.HandleError(c, "Failed to create user", err)
return
}
response.Created(c, ToResponse(user))
}
func (h *Handler) Update(c *gin.Context) {
id, ok := handler.ParseID(c, "id")
if !ok { return }
var req UpdateUserRequest
if !handler.BindJSON(c, &req) { return }
user, err := h.service.Update(c.Request.Context(), id, &req)
if err != nil {
response.HandleError(c, "Failed to update user", err)
return
}
response.Success(c, ToResponse(user))
}
func (h *Handler) Delete(c *gin.Context) {
id, ok := handler.ParseID(c, "id")
if !ok { return }
if err := h.service.Delete(c.Request.Context(), id); err != nil {
response.HandleError(c, "Failed to delete user", err)
return
}
response.NoContent(c)
}
5. RESTful URL Naming (MANDATORY)
Principles
- Use nouns, not verbs: Resources, not actions
- Use plural names:
/users, not /user
- Use HTTP methods: For actions (GET, POST, etc.)
- Use nested resources: For relationships
- Use kebab-case: For multi-word resources (if needed)
Correct Examples
GET /api/users
POST /api/users
GET /api/users/:id
PATCH /api/users/:id
DELETE /api/users/:id
GET /api/users/:id/posts
POST /api/users/:id/posts
GET /api/posts/:id/comments
GET /api/users?status=active&role=admin
GET /api/posts?author_id=123&published=true
Incorrect Examples
GET /api/getUsers
POST /api/createUser
POST /api/users/delete/:id
GET /api/fetchUserById/:id
GET /api/user
POST /api/user/:id/post
GET /api/user_list
POST /api/user-create
GET /api/get_user_by_id/:id
6. Request Validation (MANDATORY)
DTO Validation
Always use binding tags for input validation:
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8,max=72"`
Age int `json:"age" binding:"omitempty,gte=0,lte=150"`
Role string `json:"role" binding:"omitempty,oneof=admin user guest"`
}
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
Bio *string `json:"bio" binding:"omitempty,max=500"`
}
Common Validation Tags
| Tag | Description | Example |
|---|
required | Field is required | binding:"required" |
omitempty | Optional field | binding:"omitempty,email" |
min=N,max=N | String length or number range | binding:"min=3,max=50" |
gte=N,lte=N | Number greater/less than | binding:"gte=0,lte=150" |
email | Valid email format | binding:"email" |
url | Valid URL format | binding:"url" |
oneof=a b c | Enum values | binding:"oneof=active inactive" |
uuid | Valid UUID format | binding:"uuid" |
datetime | Valid datetime | binding:"datetime=2006-01-02" |
Validation in Handler
func (h *Handler) Create(c *gin.Context) {
var req CreateUserRequest
if !handler.BindJSON(c, &req) {
return
}
user, err := h.service.Create(c.Request.Context(), &req)
}
🚀 Complete CRUD Example
See examples/complete-crud-handler.go for a full implementation.
✅ Verification Checklist
Use this checklist before submitting API code:
Pagination
Error Handling
Success Responses
HTTP Methods
RESTful URLs
Validation
🔧 Automated Validation
Run the API standards validation script:
.agent/skills/api-development/scripts/validate-api.sh user
This checks:
- ✅ Pagination usage in list endpoints
- ✅
response.* usage (no manual status codes)
- ✅ RESTful URL patterns
- ✅ HTTP method correctness
- ✅ Validation tags in DTOs
🚫 Common Mistakes
Mistake 1: No Pagination
func (h *Handler) List(c *gin.Context) {
var users []User
h.db.Find(&users)
response.Success(c, users)
}
func (h *Handler) List(c *gin.Context) {
users, paginator, _ := pagination.PaginateFromContext[*domain.User](c, h.db)
response.Success(c, paginator)
}
Mistake 2: Manual Status Codes
c.JSON(404, gin.H{"error": "not found"})
c.AbortWithStatusJSON(400, map[string]any{"error": "bad request"})
response.NotFound(c, "User not found", err)
response.BadRequest(c, "Invalid input", err)
Mistake 3: Verbs in URLs
GET /api/getUsers
POST /api/createUser
POST /api/users/delete/:id
GET /api/users
POST /api/users
DELETE /api/users/:id
Mistake 4: Wrong HTTP Methods
GET /api/users/:id/update
PATCH /api/users/:id
Mistake 5: Returning 200 on DELETE
func (h *Handler) Delete(c *gin.Context) {
h.service.Delete(c.Request.Context(), id)
response.Success(c, gin.H{"message": "deleted"})
}
func (h *Handler) Delete(c *gin.Context) {
h.service.Delete(c.Request.Context(), id)
response.NoContent(c)
}
📚 Additional Resources
🔗 Related Skills
📝 Quick Reference
users, paginator, _ := pagination.PaginateFromContext[T](c, db)
response.Success(c, paginator)
response.HandleError(c, "message", err)
response.NotFound(c, "message", err)
response.BadRequest(c, "message", err)
response.Success(c, data)
response.Created(c, resource)
response.NoContent(c)
if !handler.BindJSON(c, &req) { return }
Version: 1.0.0
Last Updated: 2026-01-24
Maintainer: ZGO Team