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)
			}
		}
	}
}

output

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)
			}
		}
	}
}

output

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)
}

output