Skip to content

JSON Responses

In Express you call res.status(200).json(payload). In Echo the equivalent is c.JSON(statusCode, payload). Echo sets the Content-Type: application/json header automatically.

TypeScript
// Express
app.get('/books/:id', (req, res) => {
const book = { id: req.params.id, title: 'Go in Action' };
res.status(200).json(book);
});
// 404
res.status(404).json({ message: 'not found' });
Go
// Echo
func getBook(c echo.Context) error {
book := map[string]string{
"id": c.Param("id"),
"title": "Go in Action",
}
return c.JSON(http.StatusOK, book)
}
// 404 (via HTTPError — covered in Error Handling)
return echo.NewHTTPError(http.StatusNotFound, "not found")

Using map[string]string is convenient for examples but production code should use typed response structs so the compiler catches field-name typos and the API surface is documented in code.

TypeScript
// TypeScript — response type
interface BookResponse {
id: string;
title: string;
author: string;
publishedAt: string; // ISO 8601
}
const toResponse = (b: Book): BookResponse => ({
id: b.id,
title: b.title,
author: b.author,
publishedAt: b.createdAt.toISOString(),
});
res.json(toResponse(book));
Go
// Go — response struct
type BookResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
PublishedAt string `json:"publishedAt"` // RFC 3339 / ISO 8601
}
func toBookResponse(b Book) BookResponse {
return BookResponse{
ID: b.ID,
Title: b.Title,
Author: b.Author,
PublishedAt: b.CreatedAt.UTC().Format(time.RFC3339),
}
}
return c.JSON(http.StatusOK, toBookResponse(book))

Echo re-exports Go’s net/http constants. Use named constants instead of bare integers.

http.StatusOK // 200
http.StatusCreated // 201
http.StatusNoContent // 204
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusForbidden // 403
http.StatusNotFound // 404
http.StatusConflict // 409
http.StatusUnprocessableEntity // 422
http.StatusInternalServerError // 500

encoding/json is the engine behind c.JSON. Explore it directly:

package main
import (
"encoding/json"
"fmt"
"time"
)
type BookResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
PublishedAt time.Time `json:"publishedAt"`
}
func main() {
book := BookResponse{
ID: "abc-123",
Title: "The Go Programming Language",
Author: "Donovan & Kernighan",
PublishedAt: time.Date(2015, 10, 26, 0, 0, 0, 0, time.UTC),
}
// Marshal to JSON (what c.JSON does internally)
data, err := json.MarshalIndent(book, "", " ")
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(data))
// Unmarshal back
var decoded BookResponse
if err := json.Unmarshal(data, &decoded); err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Printf("\nDecoded title: %s\n", decoded.Title)
fmt.Printf("Published: %s\n", decoded.PublishedAt.Format(time.RFC3339))
}

Never return a raw JSON array at the top level — wrap it in an object so you can add metadata later without a breaking change.

TypeScript
// Express — paginated list
res.json({
data: books,
total: count,
page: Number(req.query.page ?? 1),
limit: Number(req.query.limit ?? 10),
});
Go
// Echo — paginated list
type PaginatedBooks struct {
Data []BookResponse `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
return c.JSON(http.StatusOK, PaginatedBooks{
Data: responses,
Total: total,
Page: page,
Limit: limit,
})
What does c.JSON(http.StatusOK, payload) do in Echo?
Which Echo method sends a response with no body and a 204 status?
Why should list endpoints wrap the array in an object (e.g. { data: [...] })?