Step 01 of 06
Learn the concept
The first client that parses your JSON turns a field name into a promise. Rename an internal field casually and you have created a breaking change with better indentation. Contracts deserve their own types.
The ideas this is made of
Struct tags are contract names
Go exports StartedAt; JSON clients expect startedAt. A tag like json:"startedAt" states the wire name explicitly. Without tags, Go's field names leak into the API. That may be acceptable for a throwaway script. Beacon is not a throwaway script.
`omitempty` can erase important truth
omitempty removes zero values, empty strings, nil slices and false booleans. That is useful for optional fields such as error. It is dangerous for statusCode if 0 means no HTTP response existed and clients need to distinguish it from an omitted field. Use pointers when absence and zero are different facts.
Time formatting is a boundary decision
Go encodes time.Time as RFC3339 by default, such as 2026-10-05T16:43:00Z. Keep UTC at the boundary and avoid local display formats in the API. Humans can render time zones later. Machines need one unambiguous instant.
Path versioning buys room
/v1/results says the response shape is version one. A future incompatible shape can live at /v2 while older clients keep working. Versioning every field is not needed. Versioning the path early is a small cost that avoids negotiation during an outage.
package main
import (
"encoding/json"
"os"
"time"
)
type Response struct {
CheckedAt time.Time `json:"checkedAt"`
Error string `json:"error,omitempty"`
}
func main() {
_ = json.NewEncoder(os.Stdout).Encode(Response{CheckedAt: time.Unix(0, 0).UTC()})
}The zero Unix instant appears as an RFC3339 string and error is absent because it is optional. That absence is now part of the contract.
Internal versus API type
| Type | Optimized for | Can change? |
|---|---|---|
Prober result | Measurement | With code |
Database row | Storage query | With migration |
API DTO | Client contract | Only by version |
Log record | Operations search | Independently |
What these are called on the job
DTO — Data transfer object: a type shaped for crossing a boundary.
Wire format — The exact bytes and field names clients receive.
Backward compatible — A change old clients can accept without modification.
