---
title: "GN006 – Return Correct HTTP Status Codes"
impact: medium
impactDescription: "Returning 200 for all responses breaks API clients, hides errors in monitoring, and makes retry logic impossible."
tags: [go, gin, api, http]
---

# GN006 – Return Correct HTTP Status Codes

## Rule

Always pass the correct HTTP status code to `c.JSON()`, `c.AbortWithStatusJSON()`, and `c.Status()`. Never return `200 OK` for errors, resource creation, or empty responses.

## Why

HTTP semantics are the contract between your API and its clients. A `200` with `{"error": "not found"}` is invisible to load balancers, monitoring tools, and client SDKs. Correct status codes enable automatic retry logic, circuit breakers, and accurate error dashboards.

## Wrong

```go
func (h *UserHandler) Create(c *gin.Context) {
    // ...
    user, err := h.service.CreateUser(ctx, req)
    if err != nil {
        c.JSON(200, gin.H{"error": err.Error()})   // ❌ success code + error body
        return
    }
    c.JSON(200, user)   // ❌ should be 201 for newly created resource
}

func (h *UserHandler) Delete(c *gin.Context) {
    h.service.DeleteUser(ctx, id)
    c.JSON(200, gin.H{"message": "deleted"})   // ❌ should be 204 with no body
}
```

## Correct

```go
import "net/http"

func (h *UserHandler) Create(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})  // 400
        return
    }
    user, err := h.service.CreateUser(c.Request.Context(), req)
    if err != nil {
        c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"})  // 500
        return
    }
    c.JSON(http.StatusCreated, user)   // 201 ✅
}

func (h *UserHandler) GetUser(c *gin.Context) {
    user, err := h.service.FindUserByID(c.Request.Context(), c.Param("id"))
    if errors.Is(err, ErrNotFound) {
        c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "user not found"})  // 404
        return
    }
    c.JSON(http.StatusOK, user)   // 200
}

func (h *UserHandler) Delete(c *gin.Context) {
    if err := h.service.DeleteUser(c.Request.Context(), c.Param("id")); err != nil {
        c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})  // 404
        return
    }
    c.Status(http.StatusNoContent)   // 204, no body ✅
}
```

## Status code reference

| Scenario | Code | Constant |
|---|---|---|
| Read success | 200 | `http.StatusOK` |
| Create success | 201 | `http.StatusCreated` |
| Delete / no body | 204 | `http.StatusNoContent` |
| Validation failed | 400 | `http.StatusBadRequest` |
| Not authenticated | 401 | `http.StatusUnauthorized` |
| Not authorized | 403 | `http.StatusForbidden` |
| Not found | 404 | `http.StatusNotFound` |
| Business rule violation | 422 | `http.StatusUnprocessableEntity` |
| Rate limited | 429 | `http.StatusTooManyRequests` |

## Notes

- Use `net/http` constants — never magic numbers like `c.JSON(201, ...)`.
- Map service-layer sentinel errors (e.g. `ErrNotFound`, `ErrConflict`) to status codes in a central error-mapping function.
