원클릭으로
api-development
ZGO API development standards including pagination, error handling, and RESTful design
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
ZGO API development standards including pagination, error handling, and RESTful design
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Test patterns, mocking strategies, and organization best practices
Standardized error format, error code clusters, and API client usage for consistent error handling.
Specifications for Zustand stores, React Query hooks, and the Service-Hook-Type pattern with optimistic updates.
Strict rules for environment variable management using Zod validation and src/config/env.ts.
Guidelines for managing internationalization (i18n) in the project using next-intl and unified translation patterns.
High-level overview of project structure, mock API architecture, and authentication flow.
| 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" |
This skill provides comprehensive API development standards for the ZGO Go backend project, ensuring consistent and high-quality REST APIs across all modules.
Rule: All list/collection endpoints MUST implement pagination.
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"
)
// ✅ CORRECT - With pagination
func (h *Handler) List(c *gin.Context) {
// One-liner pagination (recommended)
users, paginator, err := pagination.PaginateFromContext[*domain.User](c, h.db)
if err != nil {
response.HandleError(c, "Failed to fetch users", err)
return
}
// Auto-detects pagination and includes meta + links
response.Success(c, paginator)
}
// ❌ WRONG - Without pagination
func (h *Handler) List(c *gin.Context) {
var users []User
h.db.Find(&users) // Loads ALL records!
response.Success(c, users)
}
func (h *Handler) List(c *gin.Context) {
// Apply filters before pagination
query := h.db.Model(&UserPO{})
// Filter by status
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
// Filter by search
if search := c.Query("search"); search != "" {
query = query.Where("username LIKE ? OR email LIKE ?",
"%"+search+"%", "%"+search+"%")
}
// Paginate filtered results
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)
}
| 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"
}
}
Rule: All error responses MUST use pkg/response package functions.
import "github.com/zgiai/zgo/pkg/response"
// Client Errors (4xx)
response.BadRequest(c, "message", err) // 400
response.Unauthorized(c) // 401
response.Forbidden(c) // 403
response.NotFound(c, "message", err) // 404
response.Conflict(c, "message", err) // 409
response.UnprocessableEntity(c, "message", err) // 422
response.ValidationFailed(c, fieldErrors) // 422 with fields
// Server Errors (5xx)
response.InternalServerError(c, "message", err) // 500
response.ServiceUnavailable(c) // 503
// Auto-detection (recommended)
response.HandleError(c, "message", err) // Auto-maps based on error
func (h *Handler) Get(c *gin.Context) {
// Step 1: Parse and validate input
id, ok := handler.ParseID(c, "id")
if !ok {
return // ✅ 400 already sent by ParseID
}
// Step 2: Call service
user, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
// ✅ Auto-maps error type to status code
response.HandleError(c, "User not found", err)
return
}
// Step 3: Return success
response.Success(c, ToResponse(user))
}
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"]
}
}
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")
)
// Service layer
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
}
// Handler layer
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 {
// Maps custom errors to appropriate status codes
response.HandleError(c, "Login failed", err)
return
}
// ... password verification
}
Rule: Use pkg/response for all successful responses.
// 200 OK - Standard success
response.Success(c, data)
response.Success(c, paginator) // Auto-detects pagination
// 201 Created - After creating a resource
response.Created(c, newUser)
// 204 No Content - After deletion
response.NoContent(c)
// 202 Accepted - For async operations
response.Accepted(c, task)
{
"code": 0,
"message": "success",
"data": {
"id": 123,
"username": "john_doe",
"email": "john@example.com"
}
}
| 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 |
// GET - Retrieve single resource
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)) // 200
}
// POST - Create new resource
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)) // 201
}
// PATCH - Partial update
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)) // 200
}
// DELETE - Remove resource
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) // 204 No Content
}
/users, not /user// ✅ CORRECT - RESTful resource design
GET /api/users // List all users
POST /api/users // Create new user
GET /api/users/:id // Get specific user
PATCH /api/users/:id // Update user
DELETE /api/users/:id // Delete user
// Nested resources
GET /api/users/:id/posts // Get user's posts
POST /api/users/:id/posts // Create post for user
GET /api/posts/:id/comments // Get post's comments
// With query parameters
GET /api/users?status=active&role=admin
GET /api/posts?author_id=123&published=true
// ❌ WRONG - Using verbs
GET /api/getUsers
POST /api/createUser
POST /api/users/delete/:id
GET /api/fetchUserById/:id
// ❌ WRONG - Using singular
GET /api/user
POST /api/user/:id/post
// ❌ WRONG - Non-RESTful patterns
GET /api/user_list
POST /api/user-create
GET /api/get_user_by_id/:id
Always use binding tags for input validation:
// Create Request
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"`
}
// Update Request (optional fields use pointers)
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"`
}
| 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" |
func (h *Handler) Create(c *gin.Context) {
var req CreateUserRequest
// ✅ Automatic validation
if !handler.BindJSON(c, &req) {
return // 422 with field errors already sent
}
// Continue with business logic
user, err := h.service.Create(c.Request.Context(), &req)
// ...
}
See examples/complete-crud-handler.go for a full implementation.
Use this checklist before submitting API code:
pagination.PaginateFromContext[T]()meta (total, page, pageSize, totalPages)links (first, last, prev, next)page and page_sizeresponse.* functionsc.JSON(statusCode, ...) for errorsresponse.Success() for 200 OKresponse.Created() for 201 Createdresponse.NoContent() for 204 No Contentresponse.Accepted() for 202 Accepted/users, not /user)/parent/:id/child patternbinding tagshandler.BindJSON() for auto-validationRun the API standards validation script:
.agent/skills/api-development/scripts/validate-api.sh user
This checks:
response.* usage (no manual status codes)// ❌ WRONG - Returns all records
func (h *Handler) List(c *gin.Context) {
var users []User
h.db.Find(&users)
response.Success(c, users)
}
// ✅ CORRECT - With pagination
func (h *Handler) List(c *gin.Context) {
users, paginator, _ := pagination.PaginateFromContext[*domain.User](c, h.db)
response.Success(c, paginator)
}
// ❌ WRONG
c.JSON(404, gin.H{"error": "not found"})
c.AbortWithStatusJSON(400, map[string]any{"error": "bad request"})
// ✅ CORRECT
response.NotFound(c, "User not found", err)
response.BadRequest(c, "Invalid input", err)
// ❌ WRONG
GET /api/getUsers
POST /api/createUser
POST /api/users/delete/:id
// ✅ CORRECT
GET /api/users
POST /api/users
DELETE /api/users/:id
// ❌ WRONG - GET for updates
GET /api/users/:id/update
// ✅ CORRECT - PATCH for updates
PATCH /api/users/:id
// ❌ WRONG - DELETE returns data
func (h *Handler) Delete(c *gin.Context) {
h.service.Delete(c.Request.Context(), id)
response.Success(c, gin.H{"message": "deleted"}) // 200
}
// ✅ CORRECT - DELETE returns 204
func (h *Handler) Delete(c *gin.Context) {
h.service.Delete(c.Request.Context(), id)
response.NoContent(c) // 204
}
module-creation: For creating new modules with handlerscoding-standards: For general code quality standardstesting-strategy: For API testing patterns// Pagination (MUST)
users, paginator, _ := pagination.PaginateFromContext[T](c, db)
response.Success(c, paginator)
// Errors (MUST)
response.HandleError(c, "message", err) // Auto-map
response.NotFound(c, "message", err) // 404
response.BadRequest(c, "message", err) // 400
// Success (MUST)
response.Success(c, data) // 200
response.Created(c, resource) // 201
response.NoContent(c) // 204
// Validation (MUST)
if !handler.BindJSON(c, &req) { return }
Version: 1.0.0
Last Updated: 2026-01-24
Maintainer: ZGO Team