How to Add an API Endpoint
Every new endpoint must support both HTML and JSON responses via the dual-frontend architecture.
Implementation pattern
func YourHandler(c *gin.Context) {
isJSON := shouldReturnJSON(c)
// Get authenticated user
userInterface, ok := c.Get("User")
if !ok {
if isJSON {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"error": gin.H{
"code": "NOT_FOUND",
"message": "User not found in context",
},
})
} else {
c.HTML(http.StatusNotFound, "404.html", gin.H{
"title": "Wippidu - requested data not found...",
})
}
return
}
user := userInterface.(*model.User)
// ... business logic ...
// Success response
if isJSON {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"key": value,
},
})
return
}
c.HTML(http.StatusOK, "template.html", gin.H{
"title": "Page Title",
"user": user,
"data": data,
})
}
Content negotiation
The shouldReturnJSON() helper (defined in internal/controller/child.go) determines the response format:
func shouldReturnJSON(c *gin.Context) bool {
// Check if route starts with /api/
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
return true
}
// Check Accept header
if c.GetHeader("Accept") == "application/json" {
return true
}
return false
}
Register routes
HTML routes
In internal/route/auth.go under MainRoutes():
func MainRoutes(r *gin.Engine) {
// ...
r.GET("/your-route/:id", controller.YourHandler)
}
JSON API routes
In internal/route/auth.go under APIRoutes():
func APIRoutes(r *gin.Engine) {
api := r.Group("/api/v1")
{
// ...
api.GET("/your-route/:id", controller.YourHandler)
}
}
Use the same controller handler for both routes.
Required tests
Every endpoint needs at minimum:
JSON API tests
// Test successful JSON response via /api/v1/ route
func TestYourHandler_JSONAPI_Success_APIRoute(t *testing.T) { ... }
// Test JSON response via Accept header
func TestYourHandler_JSONAPI_Success_AcceptHeader(t *testing.T) { ... }
// Test error responses
func TestYourHandler_JSONAPI_NotFound(t *testing.T) { ... }
func TestYourHandler_JSONAPI_Unauthorized(t *testing.T) { ... }
func TestYourHandler_JSONAPI_InvalidInput(t *testing.T) { ... }
Database logic tests
func TestYourHandler_DatabaseLogic_ValidData(t *testing.T) { ... }
Test organization
Place tests in internal/controller/*_test.go. Group by function:
Test{Handler}_DatabaseLogic_*— Database/business logicTest{Handler}_JSONAPI_*— JSON API HTTP-levelTest{Handler}_Authorization_*— Authorization checks
Reference implementations
| Controller | What to study |
|---|---|
child.go |
Simple resource retrieval, authorization checks |
auth.go |
Login with JWT, logout, error handling |
home.go |
Handling unauthenticated users |
notify.go |
Multiple functions, parameter handling |
Checklist
- [ ] Add
isJSON := shouldReturnJSON(c)at function start - [ ] Implement JSON error responses for all error cases
- [ ] Implement JSON success response with
{"success": true, "data": {...}} - [ ] Implement HTML error responses (templates)
- [ ] Implement HTML success response (templates)
- [ ] Register route in
MainRoutes()for HTML - [ ] Register route in
APIRoutes()under/api/v1/for JSON - [ ] Write JSON API tests (at least 4-5 tests covering success, errors, auth)
- [ ] Write database logic tests
- [ ] Run
go test ./...and ensure 100% pass rate - [ ] Test manually with both browser and API client (curl/Postman)
Best practices
- Check authorization before resource existence — Return 403 even for non-existent resources to prevent information leakage
- Use consistent error codes — See API Response Formats
- Don't expose sensitive information in error messages — Generic messages like "Invalid email or password"
- Set cookies for both HTML and JSON — Enables session continuity
- Test both content negotiation methods — Both
/api/v1/routes andAcceptheader - Maintain backward compatibility — Never break existing HTML routes