| name | itsm-backend-guide |
| description | Comprehensive guide for ITSM backend development including Ent ORM workflow, Gin controller patterns, DTO validation, and service layer logic. Invoke when user asks for backend guidance. |
ITSM Backend Development Guide
This skill encapsulates the core patterns and best practices for developing the ITSM backend services using Go, Gin, and Ent ORM.
Architecture Overview
The backend follows a standard layered architecture:
- Controller Layer (
/controller): Handles HTTP requests, input validation, and response formatting.
- Service Layer (
/service): Contains business logic, transaction management, and orchestrates data operations.
- Data Access Layer (
/ent): Generated by Ent ORM for database interactions.
- DTOs (
/dto): Data Transfer Objects for request/response payloads.
Core Workflows
1. Database Schema Changes
To modify the database schema:
-
Edit ent/schema/<entity>.go.
-
Add/Modify fields using field.<Type>("name").
- Optimization: Add indexes for frequently queried fields (e.g.,
index.Fields("type")) to improve performance.
-
Run code generation:
go generate ./ent
-
Restart the application to apply auto-migrations (if enabled) or generate migration scripts.
2. DTO & Validation
- Binding Tags: Use Gin's
binding tag for validation.
required: Field must be present.
omitempty: Field is optional.
oneof=val1 val2: Enum validation (e.g., oneof=incident service_request).
- User ID Handling:
- Do NOT bind
user_id from request body for create/update operations if it should come from the auth token.
- Use
c.Get("userID") (or equivalent middleware key) to extract the authenticated user ID.
- DTO field:
UserID int \json:"user_id" binding:"omitempty"`` (allow internal setting, block external override if needed).
- JSON Field Handling:
- Structure: Define explicit Go structs for JSON fields (e.g.,
ImpactAnalysis, RootCause) instead of map[string]interface{}.
- Mapping: Use helper functions
dto.StructToMap, dto.MapToStruct, etc., to convert between DTO structs and Ent's map-based storage.
3. Controller Pattern
func (c *TicketController) UpdateTicket(ctx *gin.Context) {
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
response.Error(ctx, http.StatusBadRequest, "Invalid ID")
return
}
var req dto.UpdateTicketRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
response.Error(ctx, http.StatusBadRequest, err.Error())
return
}
req.UserID = ctx.GetInt("userID")
ticket, err := c.ticketService.UpdateTicket(ctx, id, &req)
if err != nil {
response.Error(ctx, http.StatusInternalServerError, err.Error())
return
}
response.Success(ctx, ticket)
}
4. Service Logic & Ent
- Update Logic:
- Check for nil/empty values before updating optional fields.
- Use
SetNillable<Field> if available, or if req.Field != nil { update.SetField(*req.Field) }.
- Transactions:
- Use
client.Tx(ctx) for multi-step operations (e.g., update ticket + create history record).
- Robustness:
- Lookup Failures: For non-critical associations (e.g., Category lookup by name), log a warning and proceed with defaults rather than failing the entire request.
- Tenant Isolation: ALWAYS include
.Where(entity.TenantID(tenantID)) in queries. Verify that related entities (e.g., Assignee User) also belong to the same tenant.
5. Business Logic Constraints
- Self-Approval: By default, users cannot approve their own tickets. For development/testing, this can be temporarily disabled in the Service layer, but MUST be enabled in production.
- SLA Calculation: SLA deadlines are calculated based on priority and ticket type. Ensure
SLADefinition exists for the given criteria, or provide a default fallback.
- Audit Logging: Ensure critical fields (like
type, status changes) are captured in audit logs.
Common Pitfalls & Fixes
| Issue | Cause | Fix |
|---|
| HTTP 400 Bad Request | binding:"required" on missing field (e.g., user_id) | Change to binding:"omitempty" or ensure client sends it (if appropriate). |
| HTTP 500 on Update | Nil pointer dereference in Service | Check if pointer fields in DTO are nil before accessing. |
| HTTP 500 on Status Change | Business rule violation (e.g., self-approval) | Check Service logs for specific error messages (e.g., "cannot approve own ticket"). |
| Field Mismatch | JSON tag in DTO doesn't match Frontend | Ensure json:"field_name" matches Frontend interface exactly. |
| Enum Error | Invalid value for oneof validation | Verify Frontend sends valid enum values (case-sensitive). |
| Cross-Tenant Access | Missing TenantID check | Always verify TenantID matches the context for both main and related entities. |
| API Returns 404 | Controller exists but not registered in router | Add route registration in router/router.go - see Route Registration section below |
| Missing Service Method | Calling undefined method | Add method to service layer (e.g., GetProject) before adding controller handler |
Route Registration
When adding a new module, you MUST register routes in router/router.go:
1. Add Controller to RouterConfig
ProjectController *controller.ProjectController
2. Register Routes in SetupRoutes
if config.ProjectController != nil {
projects := tenant.(*gin.RouterGroup).Group("/projects")
{
projects.GET("", config.ProjectController.ListProjects)
projects.POST("", config.ProjectController.CreateProject)
projects.GET("/:id", config.ProjectController.GetProject)
projects.PUT("/:id", config.ProjectController.UpdateProject)
projects.DELETE("/:id", config.ProjectController.DeleteProject)
}
}
3. Verify Service Methods
Before adding controller handlers, ensure service layer has the required methods:
func (s *ProjectService) GetProject(ctx context.Context, id int, tenantID int) (*ent.Project, error) {
return s.client.Project.Query().
Where(
project.IDEQ(id),
project.TenantIDEQ(tenantID),
).
First(ctx)
}
4. Verify Endpoint
curl http://localhost:8080/api/v1/projects
Testing Best Practices
Service Layer Testing
-
Service Initialization: Always use factory functions (NewTicketService(client, logger)) instead of struct literals to ensure proper initialization and avoid nil pointer dereferences.
-
Test Data Setup: Create required related entities (users, tenants) before testing business logic to avoid foreign key constraint failures.
-
Assignee Existence Check: When testing ticket creation with assignee IDs, verify the assignee user exists or create one dynamically:
if tt.request.AssigneeID > 0 {
exists, _ := client.User.Query().Where(user.ID(tt.request.AssigneeID)).Exist(ctx)
if !exists {
u, err := client.User.Create().
SetUsername(fmt.Sprintf("assignee_%d", tt.request.AssigneeID)).
SetEmail(fmt.Sprintf("assignee_%d@example.com", tt.request.AssigneeID)).
SetName("Assignee").
SetPasswordHash("hash").
SetRole("agent").
SetActive(true).
SetTenantID(tt.tenantID).
Save(ctx)
if err == nil {
tt.request.AssigneeID = u.ID
}
}
}
-
Entity Relations: Remember that related entities (like Category) are handled via edges/relations and may not be directly accessible in simple responses without eager loading.
-
Ent Query Syntax: Use generated entity packages for queries (e.g., user.ID(tt.request.AssigneeID) not ent.UserIDEQ).
Common Test Errors & Fixes
| Error | Cause | Fix |
|---|
| undefined: NewTicketService | Using struct literal initialization | Use factory function: NewTicketService(client, logger) |
| SetID undefined | Manual ID setting not allowed | Remove SetID() calls, let Ent auto-generate IDs |
| undefined: ent.UserIDEQ | Incorrect query syntax | Use user.ID() from imported entity package |
| FOREIGN KEY constraint failed | Missing related entities | Create required entities (users, tenants) before tests |
| nil pointer dereference | Improper service initialization | Use factory functions for service creation |
Debugging
- Logs: Check console output for Gin errors (e.g.,
[GIN] ... | 400 | ... | "Key: 'Field' Error:Field validation for 'Field' failed...").
- Database: Use
ent.Debug() client to see generated SQL queries.