Project challenges / verified progress
Beacon: turn a program into a service

The engineering notebook

Expose results over HTTP

How should a Go service expose stored data without turning every error into 200 OK?

Loading statusStage 5 of 10

  • Workspace not ready
  • Agent not ready
Focus25:00
A small focus ritual

0 focus sessions completed. Every fourth session offers a longer break. Start each phase when you are ready.

Study time never unlocks verified lesson progress.

Loading...

Loading verified progress...

Loading GitHub account...
Phase 3 — API and observability

Step 01 of 06

Learn the concept

A service without an API is a batch job with opinions. Operators need to ask what Beacon knows while it is running. Go's standard net/http is enough, including method-and-pattern routing added in Go 1.22.

REQUEST TO STORED DATA01ClientGET /v1/results02Middlewarelog and id03Handlervalidate query04Storelatest rows05Responsestatus plus JSON
The handler is a translator. It reads HTTP input, calls the store, and writes a chosen HTTP result. It should not know how DNS probing works or when the scheduler wakes up.
Step 01

The ideas this is made of

A handler is a boundary object

http.HandlerFunc receives a request and a response writer. Everything inside is boundary work: parse parameters, call application code, map errors, encode output. Keeping this layer thin prevents HTTP concerns from leaking into storage or scheduling packages.

ServeMux now understands methods

In Go 1.22, mux.HandleFunc("GET /v1/results", handler) can match both method and path. That removes a common manual if r.Method != ... block and makes the routing table read like the API. Older prefix behaviour still exists, so be precise.

Middleware wraps policy around handlers

Middleware is a function that takes a handler and returns a handler. Logging, request IDs, timeouts and panic recovery fit here because they apply across routes. Business logic does not. If middleware needs to know what a target is, it has probably wandered too far inside.

Status codes are part of the contract

If a query parameter is invalid, return 400. If the route does not exist, let the mux return 404. If storage fails, return 503 or 500 based on the cause. Do not inherit 200 OK by writing a body first and thinking later; headers are sent once.

Method-aware ServeMux
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, r.PathValue("name"))
	})
	r := httptest.NewRequest("GET", "/hello/sre", nil)
	w := httptest.NewRecorder()
	mux.ServeHTTP(w, r)
	fmt.Println(w.Code, w.Body.String())
}

The path variable comes from the mux, not a third-party router. Beacon can stay on the standard library and still have readable routes.

Common status choices

CaseCodeReason

Bad limit

400

Client can fix input

No route

404

Resource absent

Wrong method

405

Method not allowed

Store down

503

Dependency unavailable

Bug

500

Unexpected server fault

What these are called on the job

  • Handler — Code that turns one HTTP request into one HTTP response.

  • Middleware — A wrapper adding cross-cutting HTTP behaviour around handlers.

  • ServeMux — Go's request multiplexer for matching routes to handlers.