Playing with Go - APIs and HTTP
I want to play with Go a little bit. Let’s build a little server.
All the code is in https://github.com/k-candidate/dictionary-go.
I’m gonna be using https://freedictionaryapi.com/. I tried https://dictionaryapi.dev/ but it gave me 5XX errors consistently for some words.
First, I want to make the equivalent of:
curl https://freedictionaryapi.com/api/v1/entries/en/apple \
| jq -r '.entries.[].senses[] | .definition | "● \(.)\n"'
This would be:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// Define structs matching the required parts of the API JSON response
type ApiResponse struct {
Entries []Entry `json:"entries"`
}
type Entry struct {
Senses []Sense `json:"senses"`
}
type Sense struct {
Definition string `json:"definition"`
}
func main() {
url := "https://freedictionaryapi.com/api/v1/entries/en/apple"
// 1. Perform the HTTP GET request (equivalent to curl)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error making request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Error: status code %d\n", resp.StatusCode)
return
}
// 2. Decode the JSON response into our Go structs
var apiData ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiData); err != nil {
fmt.Printf("Error decoding JSON: %v\n", err)
return
}
// 3. Loop and format the output (equivalent to the jq filter)
for _, entry := range apiData.Entries {
for _, sense := range entry.Senses {
if sense.Definition != "" {
// Formats with a bullet point and trailing newline
fmt.Printf("● %s\n\n", sense.Definition)
}
}
}
}

We have the word “apple” hardcoded. Time to change that and make our code accept the word as an argument.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
type ApiResponse struct {
Entries []Entry `json:"entries"`
}
type Entry struct {
Senses []Sense `json:"senses"`
}
type Sense struct {
Definition string `json:"definition"`
}
func main() {
// 1. Ensure the user provided a word argument
if len(os.Args) < 2 {
fmt.Println("Usage: go run step02.go <word>")
os.Exit(1)
}
// 2. Safely URL-encode the word argument
word := url.PathEscape(os.Args[1])
apiUrl := fmt.Sprintf("https://freedictionaryapi.com/api/v1/entries/en/%s", word)
// 3. Perform the HTTP GET request
resp, err := http.Get(apiUrl)
if err != nil {
fmt.Printf("Error making request: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
fmt.Printf("Word '%s' not found.\n", os.Args[1])
return
} else if resp.StatusCode != http.StatusOK {
fmt.Printf("Error: status code %d\n", resp.StatusCode)
return
}
// 4. Decode the JSON response
var apiData ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiData); err != nil {
fmt.Printf("Error decoding JSON: %v\n", err)
return
}
// 5. Loop and format the output
for _, entry := range apiData.Entries {
for _, sense := range entry.Senses {
if sense.Definition != "" {
fmt.Printf("● %s\n\n", sense.Definition)
}
}
}
}

Alright! Now we have to shove this in a http server with an api endpoint.
For that, we have to use Go’s standard library routing net/http.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
)
type ApiResponse struct {
Entries []Entry `json:"entries"`
}
type Entry struct {
Senses []Sense `json:"senses"`
}
type Sense struct {
Definition string `json:"definition"`
}
// ClientResponse defines what our local API returns to the user
type ClientResponse struct {
Word string `json:"word"`
Definitions []string `json:"definitions"`
Error string `json:"error,omitempty"`
}
func main() {
http.HandleFunc("GET /dictionary/{word}", handleDictionary)
fmt.Println("Server starting on http://127.0.0.1:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func handleDictionary(w http.ResponseWriter, r *http.Request) {
// Set JSON response header globally for this endpoint
w.Header().Set("Content-Type", "application/json")
// Extract the path parameter safely
wordParam := r.PathValue("word")
if wordParam == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(ClientResponse{Error: "Word parameter is required"})
return
}
// 1. Safely URL-encode the word argument
word := url.PathEscape(wordParam)
apiUrl := fmt.Sprintf("https://freedictionaryapi.com/api/v1/entries/en/%s", word)
// 2. Perform the HTTP GET request to the third-party upstream API
resp, err := http.Get(apiUrl)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(ClientResponse{Error: "Failed to fetch from upstream dictionary"})
return
}
defer resp.Body.Close()
// Handle standard upstream failure states cleanly
if resp.StatusCode == http.StatusNotFound {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(ClientResponse{Error: fmt.Sprintf("Word '%s' not found", wordParam)})
return
} else if resp.StatusCode != http.StatusOK {
w.WriteHeader(http.StatusBadGateway)
json.NewEncoder(w).Encode(ClientResponse{Error: "Upstream API returned an unexpected error"})
return
}
// 3. Decode the upstream JSON response
var apiData ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiData); err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(ClientResponse{Error: "Failed to decode upstream JSON"})
return
}
// 4. Flatten definitions array to build clean client payload
var definitions []string
for _, entry := range apiData.Entries {
for _, sense := range entry.Senses {
if sense.Definition != "" {
definitions = append(definitions, sense.Definition)
}
}
}
// 5. Construct and send back the structured response
output := ClientResponse{
Word: wordParam,
Definitions: definitions,
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(output)
}

Privacy-First Subscription
Receive a notification in your inbox whenever I publish a new post. This is a reader-focused subscription loop built for absolute privacy: you stay in control, and I do not see, track, or collect your email address.
Follow via FeedRabbit