DEV Community

Cover image for Helper 1
maybeAguy?
maybeAguy?

Posted on

Helper 1

Hey everyone,

I'm back. This was my last post: https://dev.to/silver_oof_/starting-a-cs-project-week-0-5cfh. As promised, we're now going to finish the extra work I left in Go.

So, remember the server we built? It works. But if you've been staring at your terminal, you've noticed it says absolutely nothing when a request comes in. Except the “local host is running” line. We don’t want that, we need it to log its state to us.

Why we need logs

The net/http library we're using doesn't print request logs automatically. You know, those boring lines like:

2026/10/12 18:42:10 GET / 200
Enter fullscreen mode Exit fullscreen mode

Yes, they look stupid. But they're actually important. They tell us the state of the latest request hitting our server AND give us a timed history of every request, which is how we notice bugs. (A random 500 at 3 AM is a lot easier to find when something wrote it down.)

Since the library doesn't give us this, we have to build the logging system ourselves.

Making the logs

As we know http.ResponseWriter has no method to read back the status code. We can set it, but we can't ask what it was.

So the plan is to wrap the original ResponseWriter in our own struct, and remember the status code on the way through. The idea I have for this is the following:

Handler
↓
our wrapper (writes down the status code)
↓
real ResponseWriter
↓
Client
Enter fullscreen mode Exit fullscreen mode

Step 1: The wrapper struct

type loggingResponseWriter struct {
    http.ResponseWriter
    statusCode int
}
Enter fullscreen mode Exit fullscreen mode

http.ResponseWriter is embedded without a field name. That means our struct automatically gets all its methods, so it still is a ResponseWriter. We only override the parts we care about. statusCode is where we store what we catch.

Step 2: WriteHeader()

func (w *loggingResponseWriter) WriteHeader(code int) {
    if w.statusCode != 0 {
        return
    }
    w.statusCode = code
    w.ResponseWriter.WriteHeader(code)
}
Enter fullscreen mode Exit fullscreen mode

WriteHeader() is what a handler calls to send a status code. Let's break it down:

  • First, it checks if a status code was already set. If so, it returns, because only the first status code counts in an HTTP response.
  • Otherwise, it stores the code in w.statusCode.
  • Then it calls the original ResponseWriter.WriteHeader(code) so the client still receives the status code.

So we record it AND pass it along.

Step 3: Write()

func (w *loggingResponseWriter) Write(b []byte) (int, error) {
    if w.statusCode == 0 {
        w.WriteHeader(http.StatusOK)
    }
    return w.ResponseWriter.Write(b)
}
Enter fullscreen mode Exit fullscreen mode

Write() sends the response body to the client. Remember our "Hello from Go!" handler? It never called WriteHeader() anywhere. Go quietly assumes 200 OK in that case.

Since we can't see Go's quiet assumption, we copy it ourselves: if no status code has been set when Write() is called, we call WriteHeader(http.StatusOK) first. Then we hand the body to the original Write(b) and return the bytes written and any error.

Without this step, our logger would print 0 for every successful request. Not ideal.

Step 4: The Logger middleware

func Logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        lrw := &loggingResponseWriter{ResponseWriter: w}
        next.ServeHTTP(lrw, r)
        log.Printf("%s %s %d", r.Method, r.URL.Path, lrw.statusCode)
    })
}
Enter fullscreen mode Exit fullscreen mode

This is middleware: a function that sits in front of our handler, does something, and passes the request along. It takes the next handler and returns a new one.

Inside it:

  1. We wrap the original ResponseWriter in our loggingResponseWriter.
  2. We call next.ServeHTTP(lrw, r), which runs the real handler, but with our wrapper instead of the original writer.
  3. After the handler finishes, log.Printf() prints the method, URL path, and status code.

The flow now looks like:

Browser
↓
Logger (wraps the writer)
↓
ServeMux
↓
Our handler runs
↓
Logger prints: GET / 200
↓
Browser
Enter fullscreen mode Exit fullscreen mode

Plug it in

A middleware does nothing until we use it. In main, wrap the mux when starting the server:

log.Fatal(http.ListenAndServe(":8080", Logger(mux)))
Enter fullscreen mode Exit fullscreen mode

Now every request goes through Logger before reaching the ServeMux. Run it, open localhost, and your terminal should finally print something like GET / 200. And because we used log instead of print, we also got the timestamp for free.

Yes, that was a lot of code to print one line. But the best part is that this works for every route we add from now on. We write it once and forget it.

So… that's all for this one. Next, we start building the actual features of ZeroApi. If I got anything wrong (especially around middleware), please correct me in the comments. I'm learning as I go too.

Top comments (1)

Collapse
 
silver_oof_ profile image
maybeAguy? •

Author: I forgot to add /health endpoint. The function of this endpoint is to check whether the API is running and able to respond to requests.

To implement it, we put the following code into our api/main.go

mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    json.NewEncoder(w).Encode(map[string]string{
        "status": "ok",
    })
})
Enter fullscreen mode Exit fullscreen mode

As you see, this is same as the /{$} endpoint we added previously, however, the work of this endpoint gonna be nothing important related to our project. All it will add is status:ok, for now.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.