Gin Basics #4 Responses — JSON, Status Codes, Errors
In the previous post we covered how to receive and validate requests. This post goes the other direction — how to build responses. We look at response formats beyond JSON, how to handle status codes, and a pattern for making your entire API use a consistent error format.
- #1 Getting started and your first server
- #2 Routing and handlers
- #3 Request binding and validation
- #4 Responses — JSON, status codes, errors ← this post
- #5 Middleware
- #6 Database integration (GORM)
- #7 Project structure and a mini REST API
JSON responses revisited #
The most common response is JSON. c.JSON takes a status code and data.
c.JSON(http.StatusOK, gin.H{"message": "ok"})You can also pass a struct directly. It is serialized according to its json tags.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
c.JSON(http.StatusOK, User{ID: 1, Name: "Curtis"})
// {"id":1,"name":"Curtis"}Email, which has omitempty, was dropped from the output because its value is empty. This is handy when you want to hide empty fields in the response.
If you need indented JSON, use c.IndentedJSON; to emit HTML special characters without escaping them, use c.PureJSON. For production APIs, though, you usually just use c.JSON to keep the payload small.
Status codes #
For status codes, using the constants from net/http reads better than raw numbers.
http.StatusOK // 200
http.StatusCreated // 201
http.StatusNoContent // 204
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusForbidden // 403
http.StatusNotFound // 404
http.StatusInternalServerError // 500Pick the one that matches the meaning — 201 Created when you create a new resource, 204 No Content when there is no body to return, as with a delete.
c.Status(http.StatusNoContent) // 204, no bodyResponse formats beyond JSON #
JSON is not the only response type. You can use several formats depending on the situation.
c.String(http.StatusOK, "Hello, %s", name) // plain text
c.Data(http.StatusOK, "image/png", bytes) // raw bytes
c.XML(http.StatusOK, data) // XML
c.YAML(http.StatusOK, data) // YAMLc.String takes a format string and works like fmt.Sprintf. c.Data lets you pass the Content-Type and a byte slice directly, which you use to respond with arbitrary binaries like images or PDFs.
File responses #
To serve a file as is, use c.File.
c.File("./static/report.pdf")To make the browser download it with a specified file name, use c.FileAttachment.
c.FileAttachment("./static/report.pdf", "monthly-report.pdf")This sets the Content-Disposition header so the browser opens a save dialog with the name you specified.
Redirects #
To send the client to another URL, use c.Redirect.
c.Redirect(http.StatusFound, "/login") // 302For a permanent move, use http.StatusMovedPermanently (301).
Setting headers #
You set response headers directly with c.Header.
c.Header("X-Request-ID", requestID)
c.Header("Cache-Control", "no-store")Building consistent error responses #
As an API grows, error response formats tend to drift from handler to handler. If one place returns {"error": "..."} and another returns {"message": "..."}, clients have a hard time handling them. It is better to settle on a single format.
First, define an error response struct and a helper.
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
func respondError(c *gin.Context, status int, code, message string) {
c.JSON(status, ErrorResponse{Code: code, Message: message})
}In your handlers you only call this helper.
func getUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
respondError(c, http.StatusBadRequest, "INVALID_ID", "id must be a number")
return
}
user, found := findUser(id)
if !found {
respondError(c, http.StatusNotFound, "USER_NOT_FOUND", "user not found")
return
}
c.JSON(http.StatusOK, user)
}This way every error response has the same shape. The client can branch on the code value and show message to the user. In the intermediate series we will evolve this pattern into middleware-based centralized error handling.
Abort — stopping processing #
Sometimes you need to end a request immediately inside middleware or at the start of a handler. c.AbortWithStatusJSON sends a response while preventing the rest of the handler chain from running.
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
returnWe cover the Abort family and its relationship with middleware in detail in #5 Middleware.
Wrapping up #
What this post covered:
- JSON uses
c.JSON, andomitemptyon a struct can hide empty fields - Choose status codes as standard constants like
http.StatusCreated, matched to their meaning - For a body-less response use
c.Status, for stringsc.String, for bytesc.Data - Files use
c.File, downloads usec.FileAttachment - Redirects use
c.Redirect, headers usec.Header - For error responses, settle on one format and unify it with a helper
- To end a request immediately, use
c.AbortWithStatusJSON
In the next post (#5 Middleware) we will cover how to bundle processing common to many handlers — logging, recovery, authentication — into middleware.